@cloudbeat/atlas-cli 0.3.0 → 0.4.0

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.
Files changed (41) hide show
  1. package/README.md +46 -19
  2. package/dist/api.js +20 -0
  3. package/dist/cli.js +34 -6
  4. package/dist/commands/analyze.js +118 -0
  5. package/dist/commands/capacity.js +99 -0
  6. package/dist/commands/docs.js +57 -0
  7. package/dist/commands/files.js +18 -4
  8. package/dist/commands/plan.js +100 -0
  9. package/dist/commands/record.js +11 -1
  10. package/dist/commands/results.js +13 -7
  11. package/dist/commands/schema.js +86 -0
  12. package/dist/commands/skills.js +111 -0
  13. package/dist/commands/tests.js +73 -0
  14. package/dist/docs/agent-setup.md +109 -0
  15. package/dist/docs/agent-tasks.md +169 -0
  16. package/dist/docs/ai-agents.md +90 -0
  17. package/dist/docs/auth.md +74 -0
  18. package/dist/docs/automation.md +150 -0
  19. package/dist/docs/ci.md +105 -0
  20. package/dist/docs/clusters.md +48 -0
  21. package/dist/docs/commands.md +155 -0
  22. package/dist/docs/files.md +86 -0
  23. package/dist/docs/generators.md +52 -0
  24. package/dist/docs/getting-started.md +85 -0
  25. package/dist/docs/index.json +110 -0
  26. package/dist/docs/projects.md +50 -0
  27. package/dist/docs/recorder.md +143 -0
  28. package/dist/docs/results.md +79 -0
  29. package/dist/docs/running.md +80 -0
  30. package/dist/docs/skills.md +88 -0
  31. package/dist/docs/test-plans.md +92 -0
  32. package/dist/docs/troubleshooting.md +79 -0
  33. package/dist/plan.js +250 -0
  34. package/dist/skills/AGENTS.md +70 -0
  35. package/dist/skills/atlas-analyze/SKILL.md +104 -0
  36. package/dist/skills/atlas-correlate/SKILL.md +89 -0
  37. package/dist/skills/atlas-load-test/SKILL.md +154 -0
  38. package/dist/skills/atlas-record/SKILL.md +167 -0
  39. package/dist/skills/atlas-run/SKILL.md +109 -0
  40. package/dist/skills/atlas-test-plan/SKILL.md +281 -0
  41. package/package.json +5 -3
@@ -58,6 +58,7 @@ function registerRecord(program) {
58
58
  .option('--profile <dir>', 'browser profile directory for a persistent session')
59
59
  .option('--upload', 'upload the recorded .jmx into the project when done')
60
60
  .option('-p, --project <name>', 'project to upload into (with --upload)')
61
+ .option('--json', 'machine-readable output')
61
62
  .action(async (url, opts) => {
62
63
  // Fail on an unusable --upload before spending a recording session on it.
63
64
  let project = null;
@@ -94,9 +95,18 @@ function registerRecord(program) {
94
95
  if (!fs_1.default.existsSync(opts.out)) {
95
96
  throw new ui_1.CliError(`Recorder finished but ${opts.out} was not written.`, ui_1.EXIT.ERROR);
96
97
  }
98
+ if (project)
99
+ await api_1.AtlasApi.fromConfig().uploadFiles(project.id, [opts.out]);
100
+ if (opts.json) {
101
+ (0, ui_1.json)({
102
+ out: path_1.default.resolve(opts.out),
103
+ filename: path_1.default.basename(opts.out),
104
+ uploaded: project ? project.name : null,
105
+ });
106
+ return;
107
+ }
97
108
  ui_1.log.ok(`Recorded ${chalk_1.default.bold(opts.out)}`);
98
109
  if (project) {
99
- await api_1.AtlasApi.fromConfig().uploadFiles(project.id, [opts.out]);
100
110
  ui_1.log.ok(`Uploaded ${path_1.default.basename(opts.out)} → ${project.name}`);
101
111
  ui_1.log.dim(`run it with: atlas run --project ${project.name} --jmx ${path_1.default.basename(opts.out)}`);
102
112
  }
@@ -108,20 +108,26 @@ function registerResults(program) {
108
108
  program
109
109
  .command('stop <testId>')
110
110
  .description('Stop a running test')
111
- .action(async (testId) => {
111
+ .option('--json', 'machine-readable output')
112
+ .action(async (testId, opts) => {
112
113
  const api = api_1.AtlasApi.fromConfig();
113
114
  const res = await api.stop(testId);
115
+ if (opts.json)
116
+ (0, ui_1.json)({ testId, ...res });
114
117
  if (res.unreachable?.length || res.stillRunning?.length) {
115
118
  // The API only marks a server stopped once it confirms the process is
116
119
  // gone; anything else must be surfaced, not rounded up to success.
117
- ui_1.log.warn(`Stopped on ${res.ok} server(s), but could not confirm all of them.`);
118
- if (res.unreachable?.length)
119
- ui_1.log.warn(`unreachable: ${res.unreachable.join(', ')}`);
120
- if (res.stillRunning?.length)
121
- ui_1.log.warn(`still running: ${res.stillRunning.join(', ')}`);
120
+ if (!opts.json) {
121
+ ui_1.log.warn(`Stopped on ${res.ok} server(s), but could not confirm all of them.`);
122
+ if (res.unreachable?.length)
123
+ ui_1.log.warn(`unreachable: ${res.unreachable.join(', ')}`);
124
+ if (res.stillRunning?.length)
125
+ ui_1.log.warn(`still running: ${res.stillRunning.join(', ')}`);
126
+ }
122
127
  throw new ui_1.CliError('Run not fully stopped — retry when those servers are reachable.', ui_1.EXIT.ERROR);
123
128
  }
124
- ui_1.log.ok(`Stopped ${testId} on ${res.ok} server(s).`);
129
+ if (!opts.json)
130
+ ui_1.log.ok(`Stopped ${testId} on ${res.ok} server(s).`);
125
131
  });
126
132
  }
127
133
  //# sourceMappingURL=results.js.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerSchema = registerSchema;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ui_1 = require("../ui");
9
+ function optionSchema(o) {
10
+ // Commander's `required`/`optional` describe the option's VALUE, not whether
11
+ // the option itself must be passed — name them so an agent can't read them
12
+ // as "this flag is mandatory" and start passing --json <something>.
13
+ return {
14
+ flags: o.flags,
15
+ description: o.description,
16
+ takesValue: !!o.required || !!o.optional,
17
+ valueOptional: !!o.optional,
18
+ variadic: !!o.variadic,
19
+ ...(o.defaultValue !== undefined ? { default: o.defaultValue } : {}),
20
+ };
21
+ }
22
+ function argumentSchema(a) {
23
+ return {
24
+ name: typeof a.name === 'function' ? a.name() : String(a.name ?? ''),
25
+ description: a.description ?? '',
26
+ required: !!a.required,
27
+ variadic: !!a.variadic,
28
+ };
29
+ }
30
+ function commandSchema(cmd) {
31
+ // registeredArguments is Commander 11+; keep the older private field as a
32
+ // fallback so a dependency bump can't silently empty the argument list.
33
+ const args = cmd.registeredArguments ?? cmd._args ?? [];
34
+ return {
35
+ name: cmd.name(),
36
+ description: cmd.description(),
37
+ aliases: cmd.aliases(),
38
+ arguments: args.map(argumentSchema),
39
+ options: cmd.options.map(optionSchema),
40
+ commands: cmd.commands.map(c => commandSchema(c)),
41
+ };
42
+ }
43
+ function printTree(cmds, prefix) {
44
+ for (const c of cmds) {
45
+ const args = c.arguments.map(a => (a.required ? `<${a.name}>` : `[${a.name}]`)).join(' ');
46
+ console.log(`${prefix}${chalk_1.default.bold(c.name)}${args ? ' ' + args : ''} ${chalk_1.default.dim(c.description)}`);
47
+ for (const o of c.options)
48
+ console.log(`${prefix} ${o.flags} ${chalk_1.default.dim(o.description)}`);
49
+ printTree(c.commands, prefix + ' ');
50
+ }
51
+ }
52
+ function registerSchema(program) {
53
+ program
54
+ .command('schema')
55
+ .description('Describe every command, argument and option — for scripts and AI agents')
56
+ .option('--json', 'machine-readable output')
57
+ .action((opts) => {
58
+ const { version } = require('../../package.json');
59
+ const schema = {
60
+ name: program.name(),
61
+ version,
62
+ description: program.description(),
63
+ exitCodes: {
64
+ [ui_1.EXIT.OK]: 'success',
65
+ [ui_1.EXIT.GATE_FAILED]: 'a --gate threshold was breached',
66
+ [ui_1.EXIT.USAGE]: 'usage error: bad flags, unknown project, not authenticated',
67
+ [ui_1.EXIT.ERROR]: 'the run failed, timed out, or the API was unreachable',
68
+ },
69
+ env: {
70
+ ATLAS_URL: 'control plane URL (default https://atlas.cloudbeat.io)',
71
+ ATLAS_TOKEN: 'API token — create one on the Profile page',
72
+ ATLAS_PROJECT: 'default project for --project',
73
+ ATLAS_DEBUG: 'print stack traces for unexpected failures',
74
+ },
75
+ commands: program.commands.map(c => commandSchema(c)),
76
+ };
77
+ if (opts.json) {
78
+ (0, ui_1.json)(schema);
79
+ return;
80
+ }
81
+ ui_1.log.info(`${chalk_1.default.bold(schema.name)} ${schema.version}`);
82
+ printTree(schema.commands, '');
83
+ ui_1.log.dim('Use --json for the machine-readable form.');
84
+ });
85
+ }
86
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerSkills = registerSkills;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const ui_1 = require("../ui");
12
+ /**
13
+ * Installing the guidance an AI assistant needs to drive Atlas properly.
14
+ *
15
+ * The content is written once, vendor-neutral, and shipped in the package. This
16
+ * copies it where a given assistant looks: skills under .claude/skills for
17
+ * Claude Code, AGENTS.md for everything that reads that convention.
18
+ */
19
+ /** Bundled by scripts/bundle-skills.js at build time. */
20
+ const SKILLS_DIR = path_1.default.join(__dirname, '..', 'skills');
21
+ function bundledSkills() {
22
+ if (!fs_1.default.existsSync(SKILLS_DIR))
23
+ return [];
24
+ return fs_1.default
25
+ .readdirSync(SKILLS_DIR, { withFileTypes: true })
26
+ .filter(e => e.isDirectory() && fs_1.default.existsSync(path_1.default.join(SKILLS_DIR, e.name, 'SKILL.md')))
27
+ .map(e => e.name)
28
+ .sort();
29
+ }
30
+ /** Copy one file. An existing file is left alone unless --force: it may have
31
+ * been edited for the project, and silently replacing that is worse than
32
+ * leaving it slightly out of date. */
33
+ function place(from, to, force) {
34
+ if (fs_1.default.existsSync(to) && !force)
35
+ return 'skipped';
36
+ fs_1.default.mkdirSync(path_1.default.dirname(to), { recursive: true });
37
+ fs_1.default.copyFileSync(from, to);
38
+ return 'written';
39
+ }
40
+ function registerSkills(program) {
41
+ const skills = program
42
+ .command('skills')
43
+ .description('Install the Atlas guidance for AI coding assistants');
44
+ skills
45
+ .command('list')
46
+ .description('Show the skills bundled with this CLI')
47
+ .option('--json', 'machine-readable output')
48
+ .action((opts) => {
49
+ const names = bundledSkills();
50
+ if (opts.json) {
51
+ (0, ui_1.json)({ skills: names.map(name => ({ name, source: path_1.default.join(SKILLS_DIR, name, 'SKILL.md') })) });
52
+ return;
53
+ }
54
+ if (names.length === 0) {
55
+ ui_1.log.warn('No skills are bundled with this build.');
56
+ return;
57
+ }
58
+ for (const name of names)
59
+ console.log(` ${chalk_1.default.cyan(name)}`);
60
+ });
61
+ skills
62
+ .command('install')
63
+ .description('Copy the skills into this project (or your home directory)')
64
+ .option('--user', 'install for every project, under your home directory')
65
+ .option('--project', 'install into the current project (default)')
66
+ .option('--dir <path>', 'install somewhere else entirely')
67
+ .option('--force', 'overwrite files that are already there')
68
+ .option('--json', 'machine-readable output')
69
+ .action((opts) => {
70
+ const names = bundledSkills();
71
+ if (names.length === 0) {
72
+ throw new ui_1.CliError('No skills are bundled with this build.', ui_1.EXIT.ERROR);
73
+ }
74
+ const base = opts.dir
75
+ ? path_1.default.resolve(opts.dir)
76
+ : opts.user
77
+ ? path_1.default.join(os_1.default.homedir(), '.claude')
78
+ : path_1.default.join(process.cwd(), '.claude');
79
+ const installed = [];
80
+ for (const name of names) {
81
+ const to = path_1.default.join(base, 'skills', name, 'SKILL.md');
82
+ installed.push({
83
+ name,
84
+ path: to,
85
+ status: place(path_1.default.join(SKILLS_DIR, name, 'SKILL.md'), to, !!opts.force),
86
+ });
87
+ }
88
+ // AGENTS.md is the vendor-neutral half: Copilot, Cursor and others read it
89
+ // even though they know nothing about skills. Project scope only — a home
90
+ // directory is not a project root.
91
+ let agents;
92
+ const agentsSource = path_1.default.join(SKILLS_DIR, 'AGENTS.md');
93
+ if (!opts.user && fs_1.default.existsSync(agentsSource)) {
94
+ const to = path_1.default.join(opts.dir ? base : process.cwd(), 'AGENTS.md');
95
+ agents = { name: 'AGENTS.md', path: to, status: place(agentsSource, to, !!opts.force) };
96
+ installed.push(agents);
97
+ }
98
+ if (opts.json) {
99
+ (0, ui_1.json)({ base, installed });
100
+ return;
101
+ }
102
+ for (const item of installed) {
103
+ if (item.status === 'written')
104
+ ui_1.log.ok(`${item.name} → ${item.path}`);
105
+ else
106
+ ui_1.log.dim(`${item.name} already present — left alone (use --force to replace)`);
107
+ }
108
+ ui_1.log.dim('Start a new assistant session to pick them up.');
109
+ });
110
+ }
111
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerTests = registerTests;
7
+ const cli_table3_1 = __importDefault(require("cli-table3"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const api_1 = require("../api");
10
+ const config_1 = require("../config");
11
+ const ui_1 = require("../ui");
12
+ /** Project comes from --project, ATLAS_PROJECT, or the saved default. */
13
+ function projectName(flag) {
14
+ const name = (0, config_1.loadConfig)({ project: flag }).project;
15
+ if (!name) {
16
+ throw new ui_1.CliError('No project. Pass --project <name>, set ATLAS_PROJECT, or run `atlas config set project <name>`.', ui_1.EXIT.USAGE);
17
+ }
18
+ return name;
19
+ }
20
+ /** "Jun 7, 08:19 PM" — the same stamp the dashboard's test list shows. */
21
+ function started(iso) {
22
+ return new Date(iso).toLocaleString(undefined, {
23
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
24
+ });
25
+ }
26
+ function printTests(tests) {
27
+ const table = new cli_table3_1.default({
28
+ head: ['TEST ID', 'STATUS', 'STARTED', 'USERS', 'REQUESTS', 'ERR%'].map(h => chalk_1.default.dim(h)),
29
+ colAligns: ['left', 'left', 'left', 'right', 'right', 'right'],
30
+ style: { head: [], border: [] },
31
+ });
32
+ for (const t of tests) {
33
+ table.push([
34
+ t.testId,
35
+ t.status === 'running' ? chalk_1.default.green('● running') : chalk_1.default.dim('completed'),
36
+ started(t.startedAt),
37
+ String(t.maxUsers),
38
+ String(t.totalRequests),
39
+ t.errorPct === 0 ? chalk_1.default.green('0%') : chalk_1.default.yellow(`${t.errorPct}%`),
40
+ ]);
41
+ }
42
+ console.log(table.toString());
43
+ }
44
+ function registerTests(program) {
45
+ program
46
+ .command('tests')
47
+ .description('List a project\'s recent runs, most recent first')
48
+ .option('-p, --project <name>', 'project name')
49
+ .option('--limit <n>', 'show at most this many runs')
50
+ .option('--json', 'machine-readable output')
51
+ .action(async (opts) => {
52
+ const api = api_1.AtlasApi.fromConfig();
53
+ const project = projectName(opts.project);
54
+ const { tests } = await api.tests(project);
55
+ const limit = opts.limit ? parseInt(opts.limit, 10) : undefined;
56
+ if (opts.limit && (!Number.isFinite(limit) || limit < 1)) {
57
+ throw new ui_1.CliError('--limit must be a positive integer', ui_1.EXIT.USAGE);
58
+ }
59
+ const shown = limit ? tests.slice(0, limit) : tests;
60
+ if (opts.json) {
61
+ (0, ui_1.json)({ project, tests: shown });
62
+ return;
63
+ }
64
+ if (shown.length === 0) {
65
+ // The dashboard's list only reaches back a week, so an empty result is
66
+ // "nothing recent", not "this project has never run".
67
+ ui_1.log.warn(`No runs reported for "${project}" in the last 7 days.`);
68
+ return;
69
+ }
70
+ printTests(shown);
71
+ });
72
+ }
73
+ //# sourceMappingURL=tests.js.map
@@ -0,0 +1,109 @@
1
+ # Setting It Up
2
+
3
+ Two commands to install, one to sign in. The guidance is bundled with the CLI,
4
+ so there is nothing to download separately and nothing to keep in sync by hand.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install -g @cloudbeat/atlas-cli @cloudbeat/atlas-recorder
10
+ atlas login --token <your API token>
11
+ atlas skills install
12
+ ```
13
+
14
+ Create the token on your **Profile** page in Atlas. A token acts as you, in one
15
+ account, so the agent can never reach further than you can.
16
+
17
+ `atlas skills install` arrived in 0.4.0. Check with `atlas --version`, and
18
+ upgrade with `npm install -g @cloudbeat/atlas-cli@latest`.
19
+
20
+ The recorder is a separate package because it downloads its own browser. Install
21
+ it when you want an agent to record scenarios; skip it in CI, where nothing
22
+ opens a browser.
23
+
24
+ ## What gets installed
25
+
26
+ `atlas skills install` writes into the current project:
27
+
28
+ ```
29
+ .claude/skills/
30
+ atlas-load-test/SKILL.md the end-to-end flow
31
+ atlas-record/SKILL.md recording a scenario from a browser
32
+ atlas-test-plan/SKILL.md plans, data files, local debugging
33
+ atlas-correlate/SKILL.md per-session values: tokens, session ids
34
+ atlas-run/SKILL.md sizing, thresholds, CI
35
+ atlas-analyze/SKILL.md reading results, and spotting a broken test
36
+ AGENTS.md the same guidance, vendor-neutral
37
+ ```
38
+
39
+ See [The Skills](/agents/skills) for what each one covers and when it loads.
40
+
41
+ **Start a new assistant session afterwards.** Skills and `AGENTS.md` are read
42
+ when a session starts, so an open session will not see them.
43
+
44
+ ## Where to install it
45
+
46
+ | Command | Installs into | Use when |
47
+ |---|---|---|
48
+ | `atlas skills install` | the current project | The repository whose performance you are testing — the usual choice, and it can be committed so your team shares it |
49
+ | `atlas skills install --user` | your home directory | You want every project on your machine to have it |
50
+ | `atlas skills install --dir <path>` | anywhere | Unusual layouts, or a shared checkout |
51
+
52
+ Existing files are **left alone**, because you may have edited them for your
53
+ project. Pass `--force` to replace them:
54
+
55
+ ```sh
56
+ atlas skills install --force
57
+ ```
58
+
59
+ To see what a build carries before installing anything:
60
+
61
+ ```sh
62
+ atlas skills list
63
+ ```
64
+
65
+ ## Claude Code
66
+
67
+ Nothing else to do. Open Claude Code in the project and ask for a load test —
68
+ the relevant skill loads by itself when the request calls for it. You can
69
+ confirm they are there with `/skills`.
70
+
71
+ Committing `.claude/skills/` to your repository means everyone on the team gets
72
+ the same behaviour, and reviews changes to it like any other file.
73
+
74
+ ## Copilot, Cursor, Codex and friends
75
+
76
+ These read `AGENTS.md` from the project root, which `atlas skills install` writes
77
+ for you. No configuration, no extension. Commit it alongside your code.
78
+
79
+ If your assistant reads a differently named file, copy it:
80
+
81
+ ```sh
82
+ cp AGENTS.md CLAUDE.md # or .cursorrules, .github/copilot-instructions.md
83
+ ```
84
+
85
+ ## Keeping it current
86
+
87
+ The guidance ships inside the CLI, so upgrading the CLI brings new guidance with
88
+ it:
89
+
90
+ ```sh
91
+ npm install -g @cloudbeat/atlas-cli@latest
92
+ atlas skills install --force
93
+ ```
94
+
95
+ Worth doing when you upgrade, because the guidance tracks the commands. If you
96
+ have edited a skill for your own project, diff before overwriting.
97
+
98
+ ## Check it works
99
+
100
+ Ask your assistant something that needs Atlas but changes nothing:
101
+
102
+ > What can I load test here, and how much capacity do I have?
103
+
104
+ You should see it run `atlas projects` and `atlas capacity`, then answer with
105
+ your real projects and machine count. If it improvises shell commands or asks
106
+ you for infrastructure credentials, the guidance has not loaded — start a new
107
+ session and check `.claude/skills/` or `AGENTS.md` is present.
108
+
109
+ [What it can do →](/agents/capabilities)
@@ -0,0 +1,169 @@
1
+ # What an Agent Can Do
2
+
3
+ The whole lifecycle, from an empty project to a report you can send someone.
4
+ Each section below is a real thing to ask for, what the agent does about it, and
5
+ what it should ask you along the way.
6
+
7
+ ## Find its bearings
8
+
9
+ > What can I load test here, and how much capacity do I have?
10
+
11
+ Before anything else, a well-behaved assistant establishes what it is looking
12
+ at: who it is signed in as, which account is active, which projects it can
13
+ reach, which test plans and data files those hold, what ran recently, and how
14
+ much capacity and usage allowance you have left.
15
+
16
+ That last part matters more than it sounds. Sizing a test without knowing the
17
+ fleet is how people ask for 50 machines on a 20-machine account, or blow a
18
+ monthly allowance on a warm-up run.
19
+
20
+ ## Record a scenario
21
+
22
+ > Open our staging site, sign in as the demo user, add two items to the basket
23
+ > and check out. Turn that into a test plan.
24
+
25
+ The agent opens a **real, visible browser**. It reads each page as an
26
+ accessibility tree — the same structure a screen reader sees — so it clicks
27
+ "Add to basket" because that button exists, not because a selector was guessed.
28
+ It names each step *before* performing it, because those names become the rows
29
+ in your results report, and `Rome` is a useless row title.
30
+
31
+ You can watch the whole thing and take over whenever you like. For a password,
32
+ an MFA prompt, or a decision only you can make, the agent hands the window back,
33
+ you do that step, and it carries on recording.
34
+
35
+ Prefer to drive yourself? Say so, and it opens a plain recording session for you
36
+ to click through while it waits.
37
+
38
+ Export your cookies, or reuse a saved browser profile, and the recording starts
39
+ already signed in — see [Recording a Scenario](/recorder/). Never give an agent
40
+ a password to type.
41
+
42
+ ## Turn the recording into a real test
43
+
44
+ This is the step people skip, and it is the difference between a load test and
45
+ an expensive replay.
46
+
47
+ A recording is a draft: it contains exactly what *you* did. The agent reads the
48
+ capture and works through it with you, stage by stage, asking rather than
49
+ deciding:
50
+
51
+ 1. **Is the flow right?** The steps, their names, anything missing.
52
+ 2. **Which recorded values should become test data?** The username and password
53
+ you typed. The search term. The product id that appears in three different
54
+ requests. If every simulated user sends the same ones, they collide on the
55
+ same session and hit the same cache — the application looks fast because it
56
+ is barely working. The agent proposes a CSV, the columns, and how many rows;
57
+ you say whether to generate them or supply real data. Atlas then divides
58
+ those rows between machines at run time, so no two users get the same row.
59
+ 3. **What changes per session?** Session ids, CSRF tokens, cart ids — values the
60
+ *server* issued, which must be captured from an earlier response instead of
61
+ replayed. The recorder detects these with evidence, and the agent pins
62
+ anything it missed. See [Dynamic values](/recorder/#dynamic-values-are-handled).
63
+ 4. **Do you need checks?** A `200` that returns a login page is a pass as far as
64
+ JMeter is concerned. Assertions on the responses that matter turn that into a
65
+ visible failure.
66
+ 5. **Upload and smoke-run it.**
67
+
68
+ ## Edit an existing plan
69
+
70
+ > Add a "search by postcode" step to our existing plan, and point it at staging.
71
+
72
+ The agent downloads the stored plan first — uploading replaces a file and there
73
+ is no version history — then makes the narrowest change that does the job:
74
+ recording a new scenario straight into the plan as an additional thread group,
75
+ or editing specific values in place.
76
+
77
+ For structural surgery it will tell you plainly that the JMeter GUI is the right
78
+ tool, rather than rewriting a plan nobody has the source for.
79
+
80
+ ## Debug it locally, in seconds
81
+
82
+ If JMeter is installed on your machine, the agent can run a single user through
83
+ the plan locally before spending any Atlas capacity — the fastest way to confirm
84
+ a new step, an assertion or a data file actually works.
85
+
86
+ It knows the trick that makes this practical: recorded plans contain **think
87
+ time**, typically ten to thirty seconds between steps, which is correct for a
88
+ real load test and absurd for a debug run. The agent switches it off in a
89
+ throwaway copy, so a check that would have taken two minutes takes five seconds,
90
+ then restores it — your plan keeps its pauses, and nothing gets uploaded with
91
+ the think time missing.
92
+
93
+ It can also prove an assertion *fails* when it should, which is the only way to
94
+ know a check is doing anything at all.
95
+
96
+ ## Check before you spend
97
+
98
+ > Will this plan actually run?
99
+
100
+ `atlas plan check` catches what otherwise surfaces as an empty dashboard an hour
101
+ later:
102
+
103
+ - **A data file the plan reads that the project does not have.** Every user in
104
+ that thread group dies before sending a request, so the group disappears from
105
+ your results with *no errors at all* while the rest look healthy.
106
+ - **A plan Atlas cannot attach its reporting to** — it runs, and reports nothing.
107
+ - **A broken structure, or no enabled thread group.**
108
+ - **Fixed user counts**, meaning more machines is the only way to scale it.
109
+
110
+ ## Size and run it
111
+
112
+ > Run it with 500 users for ten minutes, ramped up over five.
113
+
114
+ Sizing is where load tests are most often wrong, in both directions. Each
115
+ machine runs the **whole** plan, so 100 users in the plan across 5 machines is
116
+ 500 users, not 100. The agent does that arithmetic against your real fleet,
117
+ explains it, and confirms with you before starting anything larger than a smoke
118
+ test.
119
+
120
+ It smoke-runs first — one machine, a couple of users — and checks the result
121
+ contains actual data. A smoke run that finishes in seconds with nothing in it is
122
+ a broken plan, not a fast application, and catching that costs one minute
123
+ instead of twenty machine-minutes.
124
+
125
+ While the run is live it can report progress, and stop the test if you ask.
126
+
127
+ ## Read the results
128
+
129
+ > How did it go? Where did it start to break?
130
+
131
+ Numbers are the easy part. The agent reports which steps degraded and at what
132
+ user count, what the errors actually say — with the real request and response,
133
+ not just a code — and how the run behaved over time rather than as one average.
134
+
135
+ It is also taught to distrust a clean-looking result. A test that measured
136
+ nothing looks *better* than one that worked: a missing thread group with zero
137
+ errors, a run that ended in seconds, or fast `200`s returning login pages. Those
138
+ are reported as failures of the test, not as performance.
139
+
140
+ ## Wire it into CI
141
+
142
+ > Add a nightly load test to our pipeline and fail the build if p95 goes over
143
+ > 800ms.
144
+
145
+ The agent writes the pipeline step, using thresholds and exit codes rather than
146
+ a human reading a dashboard:
147
+
148
+ ```sh
149
+ atlas run -p SHOP --jmx checkout.jmx --servers 4 \
150
+ --gate "error_rate<1,p95<800" --junit results.xml
151
+ ```
152
+
153
+ `0` passed · `1` a threshold was breached · `2` a usage or sign-in problem ·
154
+ `3` the run failed or Atlas was unreachable. JUnit output drops into whatever
155
+ already reports your test results. See [Running in CI](/cli/ci).
156
+
157
+ ## What it should always ask
158
+
159
+ - Which project, and which plan when there is more than one
160
+ - How many users, for how long, over what ramp-up
161
+ - Fixed machines or a cluster
162
+ - Confirmation before the full run
163
+ - Whether to generate test data or use yours
164
+
165
+ And what it should never do without you: start a large run, stop someone else's
166
+ test, delete a file it did not just upload, or upload a plan it has not checked.
167
+
168
+ [The skills behind this →](/agents/skills) ·
169
+ [Build your own integration →](/agents/automation)