Skip to content

Commit e1723c5

Browse files
committed
Expand unit test coverage and add TTY test infrastructure
1 parent 5f25a91 commit e1723c5

114 files changed

Lines changed: 6340 additions & 541 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/ably-codebase-review/SKILL.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,21 @@ Launch these agents **in parallel**. Each agent gets a focused mandate and uses
154154
155155
**Method (grep/glob — text patterns and file matching):**
156156
1. **Glob** for each command in `src/commands/` and check if a corresponding test file exists at `test/unit/commands/`
157-
2. **Grep** for `describe(` in test files to check for required describe blocks: `help`, `argument validation`, `functionality`, `flags`, `error handling`
158-
3. **Grep** for `--duration` in subscribe test files
157+
2. **Grep** for `describe(` in test files to check for the 5 required describe blocks with EXACT standard names:
158+
- `describe("help"` — required in every test file
159+
- `describe("argument validation"` — required (test required args OR unknown flag rejection)
160+
- `describe("functionality"` — required (core happy-path tests)
161+
- `describe("flags"` — required (verify flags exist and work)
162+
- `describe("error handling"` — required (API errors, network failures)
163+
Flag non-standard variants: `"command arguments and flags"`, `"command flags"`, `"flag options"`, `"parameter validation"`.
164+
Exception: `interactive.test.ts`, `interactive-sigint.test.ts`, and `bench/*.test.ts` are exempt.
165+
3. **Grep** for `--duration` in unit test `runCommand()` args — should NOT be present (env var handles it). Exceptions: `test:wait` tests, `interactive-sigint` tests, help output checks.
159166
4. **Grep** for `getMockAblyRealtime`, `getMockAblyRest`, `getMockConfigManager` in test files to verify correct mock usage per command type
160167
5. **Grep** for `--api-key`, `--token`, `--access-token` in unit test files — these should not use CLI auth flags
161168
162169
**Reasoning guidance:**
163170
- Missing test files are deviations but may be documented as known gaps
164-
- Missing describe blocks in existing tests are deviations but may be lower priority
171+
- Missing describe blocks or non-standard block names are deviations that should be flagged
165172
- Subscribe tests that auto-exit via mocked callbacks (without `--duration`) may be acceptable
166173
167174
### Agent 7: Lifecycle & Convention Sweep

.claude/skills/ably-new-command/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ If the new command shouldn't be available in the web CLI, add it to the appropri
363363

364364
After creating command and test files, always run:
365365
```bash
366-
pnpm prepare # Build + update manifest/README
366+
pnpm prepare # Build + update manifest
367+
pnpm exec oclif readme # Regenerate README.md from command metadata
367368
pnpm exec eslint . # Lint (must be 0 errors)
368369
pnpm test:unit # Run tests
369370
```

.claude/skills/ably-new-command/references/testing.md

Lines changed: 184 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Test files go at `test/unit/commands/<path-matching-command>.test.ts`.
44

5+
> **Note:** Do NOT pass `--duration` to `runCommand()` in unit/integration tests. `vitest.config.ts` sets `ABLY_CLI_DEFAULT_DURATION: "0.25"` which makes subscribe commands auto-exit after 250ms. Adding `--duration` overrides this with a slower value.
6+
57
## Table of Contents
68
- [Product API Test (Realtime Mock)](#product-api-test-realtime-mock)
79
- [Product API Test (REST Mock)](#product-api-test-rest-mock)
@@ -62,7 +64,7 @@ describe("topic:action command", () => {
6264

6365
describe("functionality", () => {
6466
it("should subscribe and display events", async () => {
65-
const commandPromise = runCommand(["topic:action", "test-channel", "--duration", "1"], import.meta.url);
67+
const commandPromise = runCommand(["topic:action", "test-channel"], import.meta.url);
6668

6769
await vi.waitFor(() => { expect(mockCallback).not.toBeNull(); });
6870

@@ -76,6 +78,28 @@ describe("topic:action command", () => {
7678
expect(stdout).toContain("test-channel");
7779
});
7880
});
81+
82+
describe("flags", () => {
83+
it("should accept --duration flag", async () => {
84+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
85+
expect(stdout).toContain("--duration");
86+
});
87+
});
88+
89+
describe("error handling", () => {
90+
it("should handle connection failure", async () => {
91+
const mock = getMockAblyRealtime();
92+
mock.connection.once.mockImplementation((event: string, cb: (stateChange: unknown) => void) => {
93+
if (event === "failed") cb({ reason: new Error("Connection failed") });
94+
});
95+
96+
const { error } = await runCommand(
97+
["topic:action", "test-channel"],
98+
import.meta.url,
99+
);
100+
expect(error).toBeDefined();
101+
});
102+
});
79103
});
80104
```
81105

@@ -99,13 +123,50 @@ describe("topic:action command", () => {
99123
});
100124
});
101125

102-
it("should retrieve history", async () => {
103-
const { stdout } = await runCommand(
104-
["topic:action", "test-channel"],
105-
import.meta.url,
106-
);
107-
expect(stdout).toContain("1");
108-
expect(stdout).toContain("messages");
126+
describe("help", () => {
127+
it("should display help with --help flag", async () => {
128+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
129+
expect(stdout).toContain("USAGE");
130+
});
131+
});
132+
133+
describe("argument validation", () => {
134+
it("should require channel argument", async () => {
135+
const { error } = await runCommand(["topic:action"], import.meta.url);
136+
expect(error?.message).toMatch(/Missing .* required arg/i);
137+
});
138+
});
139+
140+
describe("functionality", () => {
141+
it("should retrieve history", async () => {
142+
const { stdout } = await runCommand(
143+
["topic:action", "test-channel"],
144+
import.meta.url,
145+
);
146+
expect(stdout).toContain("1");
147+
expect(stdout).toContain("messages");
148+
});
149+
});
150+
151+
describe("flags", () => {
152+
it("should accept --json flag", async () => {
153+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
154+
expect(stdout).toContain("--json");
155+
});
156+
});
157+
158+
describe("error handling", () => {
159+
it("should handle API errors", async () => {
160+
const mock = getMockAblyRest();
161+
const channel = mock.channels._getChannel("test-channel");
162+
channel.history.mockRejectedValue(new Error("API error"));
163+
164+
const { error } = await runCommand(
165+
["topic:action", "test-channel"],
166+
import.meta.url,
167+
);
168+
expect(error).toBeDefined();
169+
});
109170
});
110171
});
111172
```
@@ -125,32 +186,61 @@ describe("topic:action command", () => {
125186
nock.cleanAll();
126187
});
127188

128-
it("should create resource", async () => {
129-
const appId = getMockConfigManager().getCurrentAppId()!;
130-
nock("https://control.ably.net")
131-
.post(`/v1/apps/${appId}/resources`)
132-
.reply(201, { id: "res-123", appId });
189+
describe("help", () => {
190+
it("should display help with --help flag", async () => {
191+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
192+
expect(stdout).toContain("USAGE");
193+
});
194+
});
133195

134-
const { stdout } = await runCommand(
135-
["topic:action", "--flag", "value"],
136-
import.meta.url,
137-
);
196+
describe("argument validation", () => {
197+
it("should reject unknown flags", async () => {
198+
const { error } = await runCommand(
199+
["topic:action", "--unknown-flag-xyz"],
200+
import.meta.url,
201+
);
202+
expect(error).toBeDefined();
203+
expect(error?.message).toMatch(/unknown|Nonexistent flag/i);
204+
});
205+
});
206+
207+
describe("functionality", () => {
208+
it("should create resource", async () => {
209+
const appId = getMockConfigManager().getCurrentAppId()!;
210+
nock("https://control.ably.net")
211+
.post(`/v1/apps/${appId}/resources`)
212+
.reply(201, { id: "res-123", appId });
213+
214+
const { stdout } = await runCommand(
215+
["topic:action", "--flag", "value"],
216+
import.meta.url,
217+
);
218+
219+
expect(stdout).toContain("created");
220+
});
221+
});
138222

139-
expect(stdout).toContain("created");
223+
describe("flags", () => {
224+
it("should accept --json flag", async () => {
225+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
226+
expect(stdout).toContain("--json");
227+
});
140228
});
141229

142-
it("should handle API errors", async () => {
143-
const appId = getMockConfigManager().getCurrentAppId()!;
144-
nock("https://control.ably.net")
145-
.post(`/v1/apps/${appId}/resources`)
146-
.reply(400, { error: "Bad request" });
230+
describe("error handling", () => {
231+
it("should handle API errors", async () => {
232+
const appId = getMockConfigManager().getCurrentAppId()!;
233+
nock("https://control.ably.net")
234+
.post(`/v1/apps/${appId}/resources`)
235+
.reply(400, { error: "Bad request" });
147236

148-
const { error } = await runCommand(
149-
["topic:action", "--flag", "value"],
150-
import.meta.url,
151-
);
237+
const { error } = await runCommand(
238+
["topic:action", "--flag", "value"],
239+
import.meta.url,
240+
);
152241

153-
expect(error).toBeDefined();
242+
expect(error).toBeDefined();
243+
});
154244
});
155245
});
156246
```
@@ -266,9 +356,69 @@ describe.skipIf(SHOULD_SKIP_E2E)("topic:action CRUD E2E", { timeout: 30_000 }, (
266356

267357
## Test Structure
268358

269-
Always include these describe blocks:
270-
1. `help` — verify `--help` shows USAGE, key flags, and EXAMPLES
271-
2. `argument validation` — verify required args produce errors when missing
272-
3. `functionality` — core behavior tests
273-
4. `flags` — verify key flags are accepted and configured
274-
5. `error handling` — API errors, invalid input
359+
Every test file MUST include all 5 of these describe blocks (exact names — no variants):
360+
361+
1. **`help`** — verify `--help` shows USAGE, key flags, and EXAMPLES
362+
2. **`argument validation`** — verify required args produce errors when missing. For commands with no required args, test that unknown flags are rejected.
363+
3. **`functionality`** — core happy-path behavior tests. This is where the main command logic is tested.
364+
4. **`flags`** — verify key flags are accepted and configured (check help output contains flag names, test flag behavior)
365+
5. **`error handling`** — API errors, invalid input, network failures
366+
367+
### Block naming rules
368+
- Use EXACTLY these names: `"help"`, `"argument validation"`, `"functionality"`, `"flags"`, `"error handling"`
369+
- Do NOT use variants like `"command arguments and flags"`, `"command flags"`, `"flag options"`, `"parameter validation"`, or domain-specific names like `"subscription behavior"`
370+
- Additional describe blocks beyond the 5 required ones are fine (e.g., `"JSON output"`, `"cleanup behavior"`)
371+
372+
### What goes in each block
373+
374+
**`argument validation`** for commands WITH required args:
375+
```typescript
376+
describe("argument validation", () => {
377+
it("should require channel argument", async () => {
378+
const { error } = await runCommand(["topic:action"], import.meta.url);
379+
expect(error?.message).toMatch(/Missing .* required arg/i);
380+
});
381+
});
382+
```
383+
384+
**`argument validation`** for commands WITHOUT required args:
385+
```typescript
386+
describe("argument validation", () => {
387+
it("should reject unknown flags", async () => {
388+
const { error } = await runCommand(
389+
["topic:action", "--unknown-flag-xyz"],
390+
import.meta.url,
391+
);
392+
expect(error).toBeDefined();
393+
expect(error?.message).toMatch(/unknown|Nonexistent flag/i);
394+
});
395+
});
396+
```
397+
398+
**`argument validation`** for topic commands (that route to subcommands):
399+
```typescript
400+
describe("argument validation", () => {
401+
it("should handle unknown subcommand gracefully", async () => {
402+
const { stdout } = await runCommand(["topic", "nonexistent"], import.meta.url);
403+
expect(stdout).toBeDefined();
404+
});
405+
});
406+
```
407+
408+
**`flags`** block pattern:
409+
```typescript
410+
describe("flags", () => {
411+
it("should show available flags in help", async () => {
412+
const { stdout } = await runCommand(["topic:action", "--help"], import.meta.url);
413+
expect(stdout).toContain("--json");
414+
});
415+
416+
it("should reject unknown flags", async () => {
417+
const { error } = await runCommand(
418+
["topic:action", "test-arg", "--unknown-flag"],
419+
import.meta.url,
420+
);
421+
expect(error).toBeDefined();
422+
});
423+
});
424+
```

.claude/skills/ably-review/SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,16 @@ For each changed command file, run the relevant checks. Spawn agents for paralle
121121
122122
### For changed test files (`test/unit/commands/**/*.ts`)
123123
124-
1. **Grep** for `describe(` to check for required describe blocks: `help`, `argument validation`, `functionality`, `flags`, `error handling`
124+
1. **Grep** for `describe(` to check for the 5 required describe blocks with EXACT standard names:
125+
- `describe("help"` — required in every test file
126+
- `describe("argument validation"` — required (test required args OR unknown flag rejection)
127+
- `describe("functionality"` — required (core happy-path tests)
128+
- `describe("flags"` — required (verify flags exist and work)
129+
- `describe("error handling"` — required (API errors, network failures)
130+
Flag any non-standard variants: `"command arguments and flags"`, `"command flags"`, `"flag options"`, `"parameter validation"`. These must use the exact standard names above.
131+
Exception: `interactive.test.ts`, `interactive-sigint.test.ts`, and `bench/*.test.ts` are exempt (REPL/benchmark tests, not command tests).
125132
2. **Grep** for `getMockAblyRealtime`, `getMockAblyRest`, `getMockConfigManager` to verify correct mock usage
126-
3. **Grep** for `--duration` in subscribe test files
133+
3. **Grep** for `--duration` in unit test `runCommand()` args — should NOT be present (env var handles it). Exceptions: `test:wait` tests, `interactive-sigint` tests, help output checks.
127134
4. **Grep** for `--api-key`, `--token`, `--access-token` — unit tests should not use CLI auth flags
128135
129136
### For new command files (added, not modified)

AGENTS.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
**Run these IN ORDER for EVERY change:**
66

77
```bash
8-
pnpm prepare # 1. Build + update manifest/README
9-
pnpm exec eslint . # 2. Lint (MUST be 0 errors)
10-
pnpm test:unit # 3. Test (at minimum)
11-
# 4. Update docs if needed
8+
pnpm prepare # 1. Build + update manifest
9+
pnpm exec oclif readme # 2. Regenerate README.md from command metadata
10+
pnpm exec eslint . # 3. Lint (MUST be 0 errors)
11+
pnpm test:unit # 4. Test (at minimum)
12+
pnpm test:tty # 5. TTY tests (local only, skip in CI)
13+
# 6. Update docs if needed
1214
```
1315

1416
**If you skip these steps, the work is NOT complete.**
@@ -164,16 +166,35 @@ runCommand(["stats", "account"], {
164166
});
165167
```
166168

169+
**Duration in testsdo NOT use `--duration` in unit/integration tests:**
170+
Unit and integration tests set `ABLY_CLI_DEFAULT_DURATION: "0.25"` in `vitest.config.ts`, which makes all subscribe/long-running commands auto-exit after 250ms. Do NOT pass `--duration` to `runCommand()`it overrides the fast 250ms default with a slower explicit value.
171+
172+
Exceptions:
173+
- `test:wait` command tests`--duration` is a required flag for that command
174+
- `interactive-sigint.test.ts`needs a longer duration for SIGINT testing
175+
- Help output checkstesting that `--help` mentions `--duration` is fine
176+
167177
**Test structure:**
168178
- `test/unit/`Fast, mocked tests. Auth via `MockConfigManager` (automatic). Only set `ABLY_API_KEY` env var when testing env var override behavior.
169179
- `test/integration/`Integration tests (e.g., interactive mode). Mocked external services but tests multi-component interaction.
180+
- `test/tty/`Interactive mode tests requiring a real pseudo-TTY (e.g., SIGINT/Ctrl+C with readline). Uses `node-pty`. Local onlycannot run in CI.
170181
- `test/e2e/`Full scenarios against real Ably. Auth via env vars (`ABLY_API_KEY`, `ABLY_ACCESS_TOKEN`).
171182
- Helpers in `test/helpers/``runCommand()`, `runLongRunningBackgroundProcess()`, `e2e-test-helper.ts`, `mock-config-manager.ts`.
172183

184+
**Required test describe blocks** (exact namesevery unit test file must have all 5):
185+
1. `"help"`verify `--help` shows USAGE
186+
2. `"argument validation"`test required args or unknown flag rejection
187+
3. `"functionality"`core happy-path behavior
188+
4. `"flags"`verify flags exist and work
189+
5. `"error handling"`API errors, network failures
190+
191+
Do NOT use variants like `"command arguments and flags"`, `"command flags"`, `"flag options"`, or `"parameter validation"`. Exempt: `interactive.test.ts`, `interactive-sigint.test.ts`, `bench/*.test.ts`.
192+
173193
**Running tests:**
174194
```bash
175195
pnpm test:unit # All unit tests
176196
pnpm test:integration # Integration tests
197+
pnpm test:tty # TTY tests (local only, needs real terminal)
177198
pnpm test:e2e # All E2E tests
178199
pnpm test test/unit/commands/foo.test.ts # Specific test
179200
```

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ All code changes, whether features or bug fixes, **MUST** follow the mandatory w
88

99
In summary, this involves:
1010

11-
1. **Build:** Run `pnpm prepare` to compile, update manifests, and update the README.
11+
1. **Build:** Run `pnpm prepare` to compile and update manifests, then `pnpm exec oclif readme` to regenerate the README.
1212
2. **Lint:** Run `pnpm exec eslint .` and fix all errors/warnings.
13-
3. **Test:** Run relevant tests (`pnpm test:unit`, `pnpm test:integration`, `pnpm test:e2e`, `pnpm test:playwright`, or specific files) and ensure they pass. Add new tests or update existing ones as needed.
13+
3. **Test:** Run relevant tests (`pnpm test:unit`, `pnpm test:integration`, `pnpm test:e2e`, `pnpm test:playwright`, or specific files) and ensure they pass. For interactive mode changes, also run `pnpm run test:tty` (requires a real terminal, not run in CI). Add new tests or update existing ones as needed.
1414
4. **Document:** Update all relevant documentation (`docs/`, `AGENTS.md`, `README.md`) to reflect your changes.
1515

1616
**Pull requests will not be merged unless all these steps are completed and verified.**

0 commit comments

Comments
 (0)