@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.
- package/README.md +46 -19
- package/dist/api.js +20 -0
- package/dist/cli.js +34 -6
- package/dist/commands/analyze.js +118 -0
- package/dist/commands/capacity.js +99 -0
- package/dist/commands/docs.js +57 -0
- package/dist/commands/files.js +18 -4
- package/dist/commands/plan.js +100 -0
- package/dist/commands/record.js +11 -1
- package/dist/commands/results.js +13 -7
- package/dist/commands/schema.js +86 -0
- package/dist/commands/skills.js +111 -0
- package/dist/commands/tests.js +73 -0
- package/dist/docs/agent-setup.md +109 -0
- package/dist/docs/agent-tasks.md +169 -0
- package/dist/docs/ai-agents.md +90 -0
- package/dist/docs/auth.md +74 -0
- package/dist/docs/automation.md +150 -0
- package/dist/docs/ci.md +105 -0
- package/dist/docs/clusters.md +48 -0
- package/dist/docs/commands.md +155 -0
- package/dist/docs/files.md +86 -0
- package/dist/docs/generators.md +52 -0
- package/dist/docs/getting-started.md +85 -0
- package/dist/docs/index.json +110 -0
- package/dist/docs/projects.md +50 -0
- package/dist/docs/recorder.md +143 -0
- package/dist/docs/results.md +79 -0
- package/dist/docs/running.md +80 -0
- package/dist/docs/skills.md +88 -0
- package/dist/docs/test-plans.md +92 -0
- package/dist/docs/troubleshooting.md +79 -0
- package/dist/plan.js +250 -0
- package/dist/skills/AGENTS.md +70 -0
- package/dist/skills/atlas-analyze/SKILL.md +104 -0
- package/dist/skills/atlas-correlate/SKILL.md +89 -0
- package/dist/skills/atlas-load-test/SKILL.md +154 -0
- package/dist/skills/atlas-record/SKILL.md +167 -0
- package/dist/skills/atlas-run/SKILL.md +109 -0
- package/dist/skills/atlas-test-plan/SKILL.md +281 -0
- package/package.json +5 -3
package/dist/commands/record.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/results.js
CHANGED
|
@@ -108,20 +108,26 @@ function registerResults(program) {
|
|
|
108
108
|
program
|
|
109
109
|
.command('stop <testId>')
|
|
110
110
|
.description('Stop a running test')
|
|
111
|
-
.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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)
|