Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flows-list-remote-env-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@qawolf/cli": minor
---

`flows list --remote` is now environment-scoped via the QA Wolf public API: pass `--env <env>` (now required with `--remote`) and optionally `--include-drafts` to include draft flows. JSON output now emits `flowId` instead of `id` for each flow.
5 changes: 5 additions & 0 deletions .changeset/log-fd-exit-race.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@qawolf/cli": patch
---

Fix a crash in the exit path when a command fails before the log file opens (a "sonic boom is not ready yet" stack trace printed after the error message). The log fd now opens eagerly at creation while writes stay asynchronous.
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@napi-rs/keyring": "1.3.0",
"@oxc-node/core": "0.1.0",
"@playwright/test": "1.60.0",
"@qawolf/api-contracts": "0.1.0",
"@qawolf/api-contracts": "0.2.0",
"@qawolf/emails": "1.1.1",
"@qawolf/flow-targets": "1.0.0",
"@qawolf/flows": "0.1.1",
Expand Down
2 changes: 1 addition & 1 deletion skills/qawolf-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ write on timeout: it may have reached the server the first time.
| `qawolf auth logout` | local | Remove stored credentials |
| `qawolf auth whoami` | read | Show authentication status |
| `qawolf doctor` | local | Diagnose problems running flows locally |
| `qawolf flows list` | local (read with --remote) | List flows matching [pattern] from the local project, or from QA Wolf with --remote |
| `qawolf flows list` | local (read with --remote) | List flows matching [pattern] from the local project, or from a QA Wolf environment with --remote |
| `qawolf flows pull` | read | Download an environment's flows into the local .qawolf/<env>/ cache |
| `qawolf flows run` | local (read with --env) | Run flows matching [pattern], or every flow when omitted; with --env, pull missing flows from that QA Wolf environment |
| `qawolf init` | local | Scaffold a QA Wolf project in the current directory |
Expand Down
19 changes: 11 additions & 8 deletions src/commands/__snapshots__/help.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Commands:
omitted; with --env, pull missing flows from that QA
Wolf environment
list [options] [pattern] List flows matching [pattern] from the local
project, or from QA Wolf with --remote
project, or from a QA Wolf environment with --remote
pull [options] Download an environment's flows into the local
.qawolf/<env>/ cache
help [command] display help for command
Expand Down Expand Up @@ -188,19 +188,22 @@ Examples:
exports[`--help output qawolf flows list 1`] = `
"Usage: qawolf flows list [options] [pattern]

List flows matching [pattern] from the local project, or from QA Wolf with
--remote
List flows matching [pattern] from the local project, or from a QA Wolf
environment with --remote

Options:
--remote List flows from the QA Wolf platform instead of the local project
(default: false)
-h, --help display help for command
--remote List flows from the QA Wolf platform instead of the local
project (default: false)
--env <env> Environment to list flows from (required with --remote)
--include-drafts Include draft flows in the listing (requires --remote)
(default: false)
-h, --help display help for command

Examples:
$ qawolf flows list
$ qawolf flows list "flows/checkout/**"
$ qawolf flows list --remote
$ qawolf flows list "**/checkout/**" --remote
$ qawolf flows list --remote --env staging
$ qawolf flows list "**/checkout/**" --remote --env staging --include-drafts
"
`;

Expand Down
69 changes: 69 additions & 0 deletions src/commands/flows/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
mock,
spyOn,
} from "bun:test";
import { Command } from "commander";

import { flowsMessages } from "~/core/messages/index.js";
import { createSignalRegistry } from "~/shell/signals/createSignalRegistry.js";

import { registerFlowsCommand } from "./index.js";

// `process.exitCode` is global state and persists across tests; reset to 0
// before/after each case so a single test setting it does not bleed into
// other test files (bun's runner reads exitCode at process exit).
beforeEach(() => {
process.exitCode = 0;
});

afterEach(() => {
process.exitCode = 0;
mock.restore();
});

function makeProgram(): Command {
const program = new Command().name("qawolf").exitOverride();
registerFlowsCommand(program, createSignalRegistry());
return program;
}

async function runList(args: string[]): Promise<string> {
const writes: string[] = [];
const capture = (chunk: unknown): boolean => {
writes.push(String(chunk));
return true;
};
spyOn(process.stdout, "write").mockImplementation(capture);
spyOn(process.stderr, "write").mockImplementation(capture);

await makeProgram().parseAsync(["flows", "list", ...args], { from: "user" });
return writes.join("");
}

describe("flows list flag combinations", () => {
it("rejects --remote without --env", async () => {
const output = await runList(["--remote"]);

expect(process.exitCode).toBe(1);
expect(output).toContain(flowsMessages.list.remoteRequiresEnv);
});

it("rejects --env without --remote", async () => {
const output = await runList(["--env", "staging"]);

expect(process.exitCode).toBe(1);
expect(output).toContain(flowsMessages.list.flagsRequireRemote);
});

it("rejects --include-drafts without --remote", async () => {
const output = await runList(["--include-drafts"]);

expect(process.exitCode).toBe(1);
expect(output).toContain(flowsMessages.list.flagsRequireRemote);
});
});
38 changes: 33 additions & 5 deletions src/commands/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Command } from "commander";

import { declareCommandKind } from "~/commands/commandKind.js";
import { withAuthContext, withContext } from "~/commands/context.js";
import { flowsMessages } from "~/core/messages/index.js";
import type { SignalRegistry } from "~/shell/signals/createSignalRegistry.js";

import { handleFlowsList } from "~/domains/flows/list.js";
Expand All @@ -17,10 +18,14 @@ const listExamples = `
Examples:
$ qawolf flows list
$ qawolf flows list "flows/checkout/**"
$ qawolf flows list --remote
$ qawolf flows list "**/checkout/**" --remote`;
$ qawolf flows list --remote --env staging
$ qawolf flows list "**/checkout/**" --remote --env staging --include-drafts`;

type FlowsListOptions = { readonly remote: boolean };
type FlowsListOptions = {
readonly remote: boolean;
readonly env: string | undefined;
readonly includeDrafts: boolean;
};

const pullExamples = `
Examples:
Expand All @@ -43,13 +48,22 @@ export function registerFlowsCommand(
kindNote: "read with --remote",
})
.description(
"List flows matching [pattern] from the local project, or from QA Wolf with --remote",
"List flows matching [pattern] from the local project, or from a QA Wolf environment with --remote",
)
.option(
"--remote",
"List flows from the QA Wolf platform instead of the local project",
false,
)
.option(
"--env <env>",
"Environment to list flows from (required with --remote)",
)
.option(
"--include-drafts",
"Include draft flows in the listing (requires --remote)",
false,
)
.addHelpText("after", listExamples)
.action(
(
Expand All @@ -58,10 +72,24 @@ export function registerFlowsCommand(
command: Command,
) => {
if (opts.remote) {
const env = opts.env;
if (env === undefined) {
return withContext(signals, async () => ({
error: flowsMessages.list.remoteRequiresEnv,
}))(opts, command);
}
return withAuthContext(signals, (ctx) =>
flowsListRemote(ctx, pattern),
flowsListRemote(ctx, pattern, {
env,
includeDrafts: opts.includeDrafts,
}),
)(opts, command);
}
if (opts.env !== undefined || opts.includeDrafts) {
return withContext(signals, async () => ({
error: flowsMessages.list.flagsRequireRemote,
}))(opts, command);
}
return withContext(signals, (ctx) => handleFlowsList(ctx, pattern))(
opts,
command,
Expand Down
5 changes: 5 additions & 0 deletions src/core/messages/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export const flowsMessages = {
title: "Flows",
remoteTitle: "Remote Flows",
flowCount: (count: number) => pluralize(count, "flow"),
list: {
remoteRequiresEnv:
"--remote requires --env <env> to pick the environment to list",
flagsRequireRemote: "--env and --include-drafts require --remote",
},
pull: {
downloadingBundle: "Downloading flows bundle",
fetchingEnvVars: "Fetching environment variables",
Expand Down
Loading
Loading