@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
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: atlas-correlate
3
+ description: Handle dynamic values in a recorded load-test scenario - CSRF tokens, session ids, anything the server issues per session - by reviewing what the Atlas recorder detected and pinning the rest. Use when a recorded test plan fails on replay, returns login pages or 403s under load, or when someone asks about correlation or parameterisation.
4
+ ---
5
+
6
+ # Dynamic values
7
+
8
+ A recording captures literals. If the server issued a value for that one session
9
+ — a CSRF token, a session id, an order reference — replaying the literal fails,
10
+ usually as a 403, a redirect back to the login page, or an application error that
11
+ only appears when the test runs.
12
+
13
+ The recorder already handles this on evidence: a value a request sends is
14
+ correlated only if that exact value appeared in an **earlier response**. That rule
15
+ keeps it from rewriting things that merely look dynamic, but it also means it
16
+ cannot catch a value the browser computed on its own.
17
+
18
+ ## Review what it found
19
+
20
+ ```bash
21
+ atlas-rec suggest flow.capture.json
22
+ ```
23
+
24
+ Each suggestion shows the variable, the value, **where it came from** (the
25
+ response that defined it, and how it will be extracted) and **who sends it**.
26
+ Judge them:
27
+
28
+ - Source is the application, extraction looks stable, consumers make sense →
29
+ keep it.
30
+ - The value is a fixed application constant that merely appeared in a response
31
+ (a country code, a page size) → correlating it is harmless but pointless.
32
+ - A value that is sent but appears in **no** earlier response → the recorder
33
+ cannot correlate it, and it is exactly the kind that breaks. Find out what it
34
+ is: generated in the browser (a timestamp, a random id, a fingerprint), or
35
+ supposed to vary per user (a login, an account number).
36
+
37
+ ## Pin the rest yourself
38
+
39
+ Write a rules file mapping parameter names to what should be sent instead:
40
+
41
+ ```json
42
+ {
43
+ "authToken": "${authToken}",
44
+ "username": "${user}",
45
+ "requestId": "${__UUID()}"
46
+ }
47
+ ```
48
+
49
+ - `${user}` — a variable the plan reads from a data file. Per-user data belongs
50
+ in a CSV, not in the plan.
51
+ - `${__UUID()}`, `${__time()}` — JMeter functions, for values that just have to
52
+ be fresh and unique.
53
+ - A literal — when the value genuinely is a constant.
54
+
55
+ Timestamps are already replaced with `${__time()}` automatically; `--no-timestamps`
56
+ turns that off.
57
+
58
+ Then rebuild the plan from the same capture — no re-recording:
59
+
60
+ ```bash
61
+ atlas-rec generate flow.capture.json -o flow.jmx --correlations rules.json
62
+ ```
63
+
64
+ Preset rules win over auto-correlation for the same parameter name, so a rule is
65
+ how you override a detection you disagree with. `--no-correlate` turns detection
66
+ off entirely and replays everything literally; that is a debugging tool, rarely
67
+ what you want.
68
+
69
+ ## Prove it before scaling
70
+
71
+ Correlation is not verified until the plan runs against the application. Upload
72
+ it and smoke-run it (see `atlas-load-test`), then:
73
+
74
+ ```bash
75
+ atlas errors <testId> --full
76
+ ```
77
+
78
+ Read the actual responses. The tells that correlation is still wrong:
79
+
80
+ - 403 / 419 / "invalid token" / "session expired" on requests that worked while
81
+ recording.
82
+ - A 200 that returns the **login page** — technically a success, functionally a
83
+ failed test. Response-time numbers from a run like that are meaningless, and
84
+ they look good, which is the dangerous part.
85
+ - The first iteration passes and later ones fail: something is being reused that
86
+ should be re-fetched per iteration.
87
+
88
+ Run it with two users and two iterations rather than one of each — a single pass
89
+ hides exactly this class of bug.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: atlas-load-test
3
+ description: Run a load test with Atlas end to end - record or pick a test plan, upload test data, size the run, do a smoke test, run it, and report the results. Use when someone asks to load-test an application, create or run a performance test, check whether a site handles N users, or interpret a test that has already run.
4
+ ---
5
+
6
+ # Load testing with Atlas
7
+
8
+ This is the ordered flow. Work through it in order, stop at each **Ask** and wait
9
+ for an answer, and never skip the smoke run. Other skills cover the detail:
10
+ `atlas-record` (capturing a scenario), `atlas-test-plan` (downloading, editing
11
+ and uploading plans and data files), `atlas-correlate` (dynamic values),
12
+ `atlas-run` (sizing and gates), `atlas-analyze` (reading results).
13
+
14
+ A load test costs real machine time on someone's account. Treat starting one the
15
+ way you would treat a deploy: confirm before you spend.
16
+
17
+ ## 0. Check the ground
18
+
19
+ ```bash
20
+ atlas whoami --json # who, which account, which instance
21
+ atlas projects --json # what this credential can see
22
+ ```
23
+
24
+ If `whoami` fails with exit code 2, the person needs to authenticate — tell them
25
+ to run `atlas login --token <token>` with a token from their Profile page. Do not
26
+ ask for their password.
27
+
28
+ If `atlas` itself is missing: `npm install -g @cloudbeat/atlas-cli`.
29
+
30
+ ## 1. Pick the project
31
+
32
+ **Ask** which project, if it is not obvious from the request or already set
33
+ (`atlas config get`). Projects are the unit that owns test plans, data files and
34
+ run history. Do not create one — that is done in the web interface.
35
+
36
+ ## 2. Get a test plan
37
+
38
+ ```bash
39
+ atlas files list -p <project> --json
40
+ ```
41
+
42
+ Three cases, and **ask** which one applies if the request does not already say:
43
+
44
+ - **Use an existing plan as it is.** When several exist, ask which — `--jmx`
45
+ chooses, and the project's default may not be the one they mean.
46
+ - **Change an existing plan**, or add a new scenario to it. Follow
47
+ `atlas-test-plan`: download it first, because uploading replaces the stored
48
+ file and there is no version history.
49
+ - **Record a new one.** Follow `atlas-record`, then `atlas-test-plan` to check
50
+ and upload it.
51
+
52
+ ## 3. Test data and a plan that will actually run
53
+
54
+ Plans usually read a CSV or TXT file per user (logins, search terms, IDs). Check
55
+ the plan against what the project holds, rather than assuming:
56
+
57
+ ```bash
58
+ atlas plan check -p <project> --jmx <plan>
59
+ ```
60
+
61
+ That reports a data file the plan reads but the project does not have — the
62
+ failure that makes an entire thread group disappear from the results with **no
63
+ errors at all**. Upload whatever is missing, and anything that has gone stale:
64
+
65
+ ```bash
66
+ atlas files upload -p <project> ./users.csv ./search-terms.txt
67
+ ```
68
+
69
+ Worth saying out loud, because it surprises people: data files are **divided
70
+ between machines**, not copied to each. Ten thousand rows across five machines is
71
+ two thousand rows each. If every machine must see every row, the run needs
72
+ `--no-split`. The rest of the file lifecycle is in `atlas-test-plan`.
73
+
74
+ ## 4. Size the run
75
+
76
+ ```bash
77
+ atlas capacity --json
78
+ ```
79
+
80
+ This gives machines, clusters, how many are free, and how much usage allowance is
81
+ left. Read it **before** asking, so the question is grounded in real numbers.
82
+
83
+ **Ask** three things:
84
+
85
+ 1. How many concurrent users should the test reach?
86
+ 2. How long should it run, and over how long should users ramp up?
87
+ 3. Fixed machines or a cluster? (Only if both are available.)
88
+
89
+ Then do the arithmetic and show your working: **each machine runs the whole plan
90
+ independently**, so total users = users-in-the-plan x machines. A plan with 100
91
+ users on 5 machines is 500 users. A plan recorded with the recorder exposes its
92
+ size as a run-time setting, so you can size it without editing the file:
93
+
94
+ ```bash
95
+ atlas run -p <project> --jmx flow.jmx --servers 5 --args "-Jthreads=100 -Jrampup=300"
96
+ ```
97
+
98
+ If the plan was not written that way, the thread counts are fixed inside the
99
+ file, and machines are the only dial you have. Say so rather than silently
100
+ running the wrong size.
101
+
102
+ ## 5. Smoke run first
103
+
104
+ Never go straight to the full run. One machine, a couple of users, a minute or
105
+ two:
106
+
107
+ ```bash
108
+ atlas run -p <project> --jmx <plan> --servers 1 --args "-Jthreads=2 -Jloops=2" --wait
109
+ ```
110
+
111
+ Then check it actually worked, which the exit code alone does not tell you:
112
+
113
+ ```bash
114
+ atlas results <testId> --json
115
+ atlas errors <testId> --json
116
+ ```
117
+
118
+ Look for: every thread group present, requests actually sent, errors understood.
119
+ A smoke run that finishes in seconds with no data is a broken plan, not a fast
120
+ application — `atlas-analyze` lists the usual causes. Fix that before scaling up.
121
+
122
+ **Ask** for confirmation before the full run, showing what you are about to
123
+ start: plan, machines, users, duration, and the smoke result.
124
+
125
+ ## 6. The full run
126
+
127
+ ```bash
128
+ atlas run -p <project> --jmx <plan> --servers <n> \
129
+ --args "-Jthreads=<per machine> -Jrampup=<seconds>" \
130
+ --wait --gate "error_rate<1,p95<2000"
131
+ ```
132
+
133
+ `--wait` blocks until it finishes; `--gate` turns the result into a pass or fail
134
+ (exit code 1 when a threshold is breached). Tell the person the test id and the
135
+ dashboard link as soon as the run starts, so they can watch it themselves.
136
+
137
+ If they do not want to wait, drop `--wait` and poll `atlas status <testId>`.
138
+
139
+ ## 7. Report
140
+
141
+ Follow `atlas-analyze`. A useful report answers: did it pass, what was the
142
+ response time under load, where did it start to degrade, what failed and why, and
143
+ what to do next. Percentiles matter more than averages, and the error detail
144
+ matters more than the error rate.
145
+
146
+ ## Guardrails
147
+
148
+ - **Confirm before starting a full run.** Machines get powered on and billed.
149
+ - **Never stop someone else's run.** `atlas tests -p <project>` shows what is
150
+ running; if something is already going, ask before touching it.
151
+ - **Report what happened, not what should have happened.** If a thread group is
152
+ missing or a gate failed, say so plainly with the numbers.
153
+ - **`atlas stop` can come back unconfirmed.** If it reports machines it could not
154
+ reach, the test may still be running — say that rather than "stopped".
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: atlas-record
3
+ description: Record a browser scenario into a JMeter test plan with the Atlas recorder, including driving the browser yourself - open pages, click, fill forms, name transactions - and rebuilding the plan from a capture afterwards. Use when someone wants to create a load-test scenario from a real user flow, or asks you to click through an application and turn it into a test.
4
+ ---
5
+
6
+ # Recording a scenario
7
+
8
+ The recorder watches a real browser session and writes a JMeter plan from it.
9
+ A person can click through the flow themselves, or you can do the clicking. Both
10
+ are recorded the same way, and both can happen in the same session.
11
+
12
+ Requires `@cloudbeat/atlas-recorder` (`npm install -g @cloudbeat/atlas-recorder`).
13
+ It downloads its own browser on install.
14
+
15
+ ## Which mode
16
+
17
+ **Ask** which the person wants, unless the request already says:
18
+
19
+ - **They click** — `atlas-rec record <url> -o flow.jmx`. Blocks until they press
20
+ Stop. Use this when the flow needs a human: a password only they know, a 2FA
21
+ prompt, a decision about what to test.
22
+ - **You click** — the commands below. Use this when the flow is mechanical and
23
+ they have told you what to do.
24
+
25
+ Either way the window is visible and they can watch, or take over.
26
+
27
+ ## Driving it yourself
28
+
29
+ ```bash
30
+ atlas-rec start https://shop.example.com -o flow.jmx -n "Checkout"
31
+ ```
32
+
33
+ Returns JSON with a `sessionId`. The browser is now open and recording. Later
34
+ commands find the session automatically when only one is running; pass
35
+ `--session <id>` when more than one is.
36
+
37
+ **Look before you act.** `snapshot` gives the page as an accessibility tree —
38
+ what is there and what can be clicked:
39
+
40
+ ```bash
41
+ atlas-rec snapshot
42
+ ```
43
+
44
+ Then act. Prefer the target the snapshot actually showed you:
45
+
46
+ ```bash
47
+ atlas-rec click --role button --name "Find Flights"
48
+ atlas-rec click --text "Choose This Flight" # by visible text
49
+ atlas-rec fill "you@example.com" --selector "#email"
50
+ atlas-rec select "Rome" --selector "select[name=toPort]"
51
+ atlas-rec press Enter --selector "#search"
52
+ atlas-rec wait --selector "table" # or --url, or --ms
53
+ atlas-rec goto https://shop.example.com/cart
54
+ ```
55
+
56
+ Add `--nth <n>` when several things match. Snapshot again after anything that
57
+ changes the page — do not assume the click landed where you expected.
58
+
59
+ ## Name the steps as you go
60
+
61
+ Each user action becomes a transaction in the plan, and transaction names are
62
+ what the results are reported against. Auto-generated names like "Rome" are
63
+ useless in a report. Name the step **before** you perform it:
64
+
65
+ ```bash
66
+ atlas-rec tx new "Search flights"
67
+ atlas-rec click --role button --name "Find Flights"
68
+ ```
69
+
70
+ Check what has been captured at any point:
71
+
72
+ ```bash
73
+ atlas-rec state # one line per transaction
74
+ atlas-rec state --full # every captured request
75
+ ```
76
+
77
+ Housekeeping while recording:
78
+
79
+ ```bash
80
+ atlas-rec tx rename t3 "Add to cart"
81
+ atlas-rec req exclude r12 # keep a request out of the plan
82
+ atlas-rec req delete r12 # drop it from the recording entirely
83
+ atlas-rec pause / atlas-rec resume
84
+ atlas-rec scenario "Checkout"
85
+ ```
86
+
87
+ ## Finishing
88
+
89
+ ```bash
90
+ atlas-rec stop
91
+ ```
92
+
93
+ This writes the plan and a capture file beside it, and returns both paths. The
94
+ plan is also written if the window is closed or the process is interrupted — a
95
+ recording is not lost by closing the browser.
96
+
97
+ ## Signing in
98
+
99
+ Three options, in order of preference:
100
+
101
+ - `--cookies <file>` — exported cookies, so the recording starts already signed
102
+ in. Accepts Cookie-Editor, Netscape, a raw `Cookie:` header, or a storage-state
103
+ file.
104
+ - `--profile <file>` — saves the signed-in session on stop and reuses it next
105
+ time. Good for repeated recordings against the same app.
106
+ - Record the login itself — but then the plan contains one user's credentials and
107
+ every simulated user will send them. That is usually wrong: a real test needs a
108
+ data file of users. Say so.
109
+
110
+ Never ask for a password to type into the browser yourself. If the flow needs
111
+ credentials, hand the window back to the person for that step: they can type it
112
+ while the recording is live, and it is captured like anything else.
113
+
114
+ ## After the recording
115
+
116
+ ```bash
117
+ atlas-rec inspect flow.capture.json # transactions, hosts, response codes
118
+ atlas-rec suggest flow.capture.json # values that change per session
119
+ ```
120
+
121
+ Read `inspect` before declaring success. Things to check:
122
+
123
+ - Every step you performed has a transaction, and the count of included requests
124
+ is not zero.
125
+ - No 4xx/5xx in `statusCodes` that you did not expect — a failed request recorded
126
+ as-is becomes a failing request in the test.
127
+ - Hosts are the application's, not a third party's. Analytics and CDN traffic is
128
+ filtered out already, but an embedded widget may not be.
129
+
130
+ ## Then stop and agree the next step
131
+
132
+ A recording is a draft, not a test. When you have stopped, **report what you
133
+ captured and ask what to do with it** — do not upload it, and do not start
134
+ editing it, before the person has answered.
135
+
136
+ Say what is there: the scenario name, each transaction with its request count,
137
+ any unexpected status codes, and where the plan and capture files are. Then work
138
+ through the stages with them — flow, test data, dynamic values, checks, upload —
139
+ asking rather than deciding. Those stages are in `atlas-test-plan`, under
140
+ "Turning a recording into a test"; **read it before suggesting what comes next**,
141
+ because the question it insists on (which recorded values every simulated user
142
+ would otherwise repeat) is what separates a load test from a replay.
143
+
144
+ Rebuild the plan from the capture rather than re-recording whenever the change is
145
+ in how the plan is generated:
146
+
147
+ ```bash
148
+ atlas-rec generate flow.capture.json -o flow.jmx --correlations rules.json
149
+ ```
150
+
151
+ The same capture always produces the same plan, so this is safe to repeat.
152
+
153
+ ## Things that will bite you
154
+
155
+ - **Never trigger a native dialog** (`alert`, `confirm`, a file picker). They
156
+ block the browser and the session stops responding. Avoid clicking anything
157
+ that might delete or confirm.
158
+ - **One click can record two requests** when a page redirects, so a step can hit
159
+ its destination twice per iteration. Check `inspect` and exclude the duplicate.
160
+ - **Everything before the first action is one transaction** — redirects, an SSO
161
+ hop, a failed load. Rename it to something meaningful.
162
+ - **The recording is one pass.** A cookie banner, an A/B variant or a retry that
163
+ did not happen while you clicked is not in the plan.
164
+ - **Recorded values are literal.** A search term, a product id, a date you picked
165
+ will be sent by every simulated user unless it is parameterised from a data
166
+ file. Flag this to the person; it is usually the difference between a realistic
167
+ test and a cached one.
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: atlas-run
3
+ description: Start and size an Atlas load test - how many machines or cluster units, how users map onto them, ramp-up, pass/fail gates, exit codes and CI usage. Use when someone asks to run a test, asks how many machines they need, wants a test to fail a build on a threshold, or is waiting on a queued run.
4
+ ---
5
+
6
+ # Running and sizing
7
+
8
+ ```bash
9
+ atlas run -p <project> --jmx <plan> --servers 5 --wait --gate "error_rate<1,p95<2000"
10
+ ```
11
+
12
+ ## How load adds up
13
+
14
+ **Each machine runs the whole plan independently.** There is no controller
15
+ splitting users between them. So:
16
+
17
+ ```
18
+ total users = users in the plan x number of machines
19
+ ```
20
+
21
+ A plan with 100 users on 5 machines is 500 concurrent users. This is the single
22
+ most common thing to get wrong, and it is wrong by a factor of the fleet size.
23
+
24
+ A plan produced by the recorder exposes its size as run-time settings, so you can
25
+ scale it without editing the file:
26
+
27
+ ```bash
28
+ atlas run -p SHOP --jmx flow.jmx --servers 5 --args "-Jthreads=100 -Jrampup=300 -Jloops=-1"
29
+ ```
30
+
31
+ - `-Jthreads` — users **per machine**
32
+ - `-Jrampup` — seconds to start them all; ramping 500 users instantly tests your
33
+ own patience, not the application
34
+ - `-Jloops` — iterations per user; `-1` is continuous
35
+
36
+ An older or hand-written plan usually has fixed numbers inside it. Then machines
37
+ are the only dial, and a plan with 2 users cannot be made into a 500-user test by
38
+ asking for 5 machines. Say so instead of running it anyway.
39
+
40
+ ## Where it runs
41
+
42
+ ```bash
43
+ atlas capacity --json
44
+ ```
45
+
46
+ - `--servers <n>` — n available machines
47
+ - `--server <name...>` — specific machines by name
48
+ - `--cluster <name> --units <n>` — elastic capacity: n containers created for the
49
+ test and destroyed afterwards
50
+
51
+ Machines that are off are fine: they are powered on for the run and powered off
52
+ afterwards. That is also why a run costs money — confirm before starting a large
53
+ one.
54
+
55
+ If every machine is busy, the run is **queued** rather than rejected, and starts
56
+ when they free up. `atlas run` says so; with `--wait` it waits through the queue.
57
+
58
+ ## Data files
59
+
60
+ Data files are **divided** between machines by default — 10,000 rows across 5
61
+ machines is 2,000 each, so no two users get the same row. Use `--no-split` when
62
+ every machine must see the whole file (a small lookup table, a list of products
63
+ every user should hit).
64
+
65
+ ## Gates
66
+
67
+ ```bash
68
+ --gate "error_rate<1,p95<2000" --gate "throughput>50"
69
+ ```
70
+
71
+ Metrics: `error_rate`, `avg`, `p90`, `p95`, `p99`, `max`, `throughput`,
72
+ `requests`, with `<`, `<=`, `>`, `>=`. They are evaluated against the whole test.
73
+ `--gate` and `--junit` imply `--wait`, because there is nothing to judge until
74
+ the run has finished.
75
+
76
+ ## Exit codes
77
+
78
+ | Code | Meaning |
79
+ |---|---|
80
+ | 0 | ran, every gate passed |
81
+ | 1 | ran, a gate was breached |
82
+ | 2 | usage error: bad flags, unknown project, not authenticated |
83
+ | 3 | the run failed or timed out, or the server was unreachable |
84
+
85
+ 1 and 3 are different in the way that matters for CI: 1 is a result, 3 is a
86
+ failure to get a result. Do not paper over either.
87
+
88
+ ## In CI
89
+
90
+ ```bash
91
+ atlas run -p SHOP --jmx flow.jmx --servers 3 --wait \
92
+ --gate "error_rate<1,p95<2000" --junit results.xml
93
+ ```
94
+
95
+ Authenticate with a token in `ATLAS_TOKEN` — never a password. A token acts as
96
+ its owner in one account, and it stops working the moment that user is removed.
97
+
98
+ ## While it runs, and stopping
99
+
100
+ ```bash
101
+ atlas status <testId> # still going? headline numbers
102
+ atlas tests -p <project> # recent runs, including anything running now
103
+ atlas stop <testId>
104
+ ```
105
+
106
+ `atlas stop` only reports success once it has **confirmed** the process is gone
107
+ on every machine. If it comes back with machines it could not reach, the test may
108
+ well still be running and reporting: say that, and retry. Treating an unconfirmed
109
+ stop as a stop is how a runaway test goes unnoticed for hours.