Skip to content

Commit 2b85fa4

Browse files
authored
Merge pull request #104 from probitas-test/feat/unknown-args-detection
Add CLI usability improvements: unknown argument detection and failed scenario re-run
2 parents eabe5c4 + 0bf2513 commit 2b85fa4

13 files changed

Lines changed: 1251 additions & 65 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.DS_Store
22
.coverage
3+
.probitas/
34
result
45
dist/
56
src/cli/_embedded_templates.ts

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ probitas run
200200
# Run scenarios with specific tag
201201
probitas run -s tag:api
202202

203+
# Re-run only failed scenarios from previous run
204+
probitas run --failed
205+
203206
# Run with JSON output
204207
probitas run --reporter json
205208

assets/usage-run.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Options:
2121
-S, --sequential Run scenarios sequentially (alias for --max-concurrency=1)
2222
--max-failures <n> Stop after N failures
2323
-f, --fail-fast Stop on first failure (alias for --max-failures=1)
24+
-F, --failed Run only scenarios that failed in the previous run
25+
Reads from .probitas/last-run.json
2426
--config <path> Path to config file (deno.json/deno.jsonc)
2527
--env <file> Load environment variables from file (default: .env)
2628
--no-env Skip loading .env file
@@ -93,6 +95,11 @@ Examples:
9395
probitas run -f Stop on first failure (alias)
9496
probitas run --reload Reload dependencies before running
9597

98+
# Re-run failed scenarios
99+
probitas run --failed Run only scenarios that failed previously
100+
probitas run -F Run only scenarios that failed previously (short)
101+
probitas run -F -s tag:api Run failed scenarios AND with @api tag
102+
96103
# Output control
97104
probitas run -v Verbose output
98105
probitas run --verbose Verbose output (same as -v)

deno.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
"@std/testing/bdd": "jsr:@std/testing@^1.0.16/bdd",
107107
"@std/testing/mock": "jsr:@std/testing@^1.0.16/mock",
108108
"@std/testing/time": "jsr:@std/testing@^1.0.16/time",
109+
"@std/text": "jsr:@std/text@^1.0.17",
110+
"@std/text/closest-string": "jsr:@std/text@^1.0.17/closest-string",
111+
"@std/text/levenshtein-distance": "jsr:@std/text@^1.0.17/levenshtein-distance",
109112
"jsr:@probitas/probitas@^0": "./src/mod.ts"
110113
}
111114
}

deno.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli/_templates/run.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ runSubprocess<RunCommandInput>(async (ipc, input) => {
6666
timeout,
6767
stepOptions,
6868
logLevel,
69+
failedFilter,
6970
} = input;
7071

7172
// Create abort controller for this run
@@ -75,8 +76,8 @@ runSubprocess<RunCommandInput>(async (ipc, input) => {
7576
await configureLogging(logLevel);
7677

7778
try {
78-
// Load scenarios from files
79-
const scenarios = applySelectors(
79+
// Load scenarios from files and apply selectors
80+
let scenarios = applySelectors(
8081
await loadScenarios(filePaths, {
8182
onImportError: (file, err) => {
8283
const m = err instanceof Error ? err.message : String(err);
@@ -86,6 +87,26 @@ runSubprocess<RunCommandInput>(async (ipc, input) => {
8687
selectors,
8788
);
8889

90+
// Apply failed filter if provided (for --failed flag)
91+
if (failedFilter && failedFilter.length > 0) {
92+
const failedSet = new Set(
93+
failedFilter.map((f) => `${f.name}|${f.file}`),
94+
);
95+
scenarios = scenarios.filter((s) => {
96+
const scenarioFile = s.origin?.path ?? "unknown";
97+
// Check both absolute path and relative path matching
98+
return failedSet.has(`${s.name}|${scenarioFile}`) ||
99+
failedFilter.some((f) =>
100+
f.name === s.name && scenarioFile.endsWith(f.file)
101+
);
102+
});
103+
104+
logger.debug("Applied failed filter", {
105+
filterCount: failedFilter.length,
106+
matchedCount: scenarios.length,
107+
});
108+
}
109+
89110
// Check if aborted during loading phase
90111
if (globalAbortController.signal.aborted) {
91112
throw new Error("Aborted during scenario loading");
@@ -96,6 +117,7 @@ runSubprocess<RunCommandInput>(async (ipc, input) => {
96117
logger.info("No scenarios found after applying selectors", {
97118
filePaths,
98119
selectors,
120+
failedFilter: failedFilter?.length ?? 0,
99121
});
100122
}
101123

src/cli/_templates/run_protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ export type RunInput =
3434
| RunCommandInput
3535
| RunAbortInput;
3636

37+
/**
38+
* Failed scenario identifier for filtering
39+
*/
40+
export interface FailedScenarioFilter {
41+
/** Scenario name */
42+
readonly name: string;
43+
/** File path (relative or absolute) */
44+
readonly file: string;
45+
}
46+
3747
/**
3848
* Run scenarios command
3949
*/
@@ -53,6 +63,8 @@ export interface RunCommandInput {
5363
readonly stepOptions?: StepOptions;
5464
/** Log level for subprocess logging */
5565
readonly logLevel: LogLevel;
66+
/** Filter to only run these failed scenarios (name + file pairs) */
67+
readonly failedFilter?: readonly FailedScenarioFilter[];
5668
}
5769

5870
/**

src/cli/commands/list.ts

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { fromErrorObject, isErrorObject } from "@core/errorutil/error-object";
1212
import { unreachable } from "@core/errorutil/unreachable";
1313
import { EXIT_CODE } from "../constants.ts";
1414
import { findProbitasConfigFile, loadConfig } from "../config.ts";
15+
import {
16+
createUnknownArgHandler,
17+
extractKnownOptions,
18+
formatUnknownArgError,
19+
} from "../unknown_args.ts";
1520
import { createDiscoveryProgress, writeStatus } from "../progress.ts";
1621
import {
1722
configureLogging,
@@ -29,6 +34,36 @@ import {
2934

3035
const logger = getLogger(["probitas", "cli", "list"]);
3136

37+
/**
38+
* parseArgs configuration for the list command
39+
*/
40+
const PARSE_ARGS_CONFIG = {
41+
string: ["config", "include", "exclude", "selector", "env"],
42+
boolean: [
43+
"help",
44+
"json",
45+
"no-env",
46+
"reload",
47+
"quiet",
48+
"verbose",
49+
"debug",
50+
],
51+
collect: ["include", "exclude", "selector"],
52+
alias: {
53+
h: "help",
54+
s: "selector",
55+
r: "reload",
56+
v: "verbose",
57+
q: "quiet",
58+
d: "debug",
59+
},
60+
default: {
61+
include: undefined,
62+
exclude: undefined,
63+
selector: undefined,
64+
},
65+
} as const;
66+
3267
/**
3368
* Execute the list command
3469
*
@@ -46,34 +81,33 @@ export async function listCommand(
4681
// Extract deno options first (before parseArgs)
4782
const { denoArgs, remainingArgs } = extractDenoOptions(args);
4883

84+
// Setup unknown argument handler
85+
const knownOptions = extractKnownOptions(PARSE_ARGS_CONFIG);
86+
const { handler: unknownHandler, result: unknownResult } =
87+
createUnknownArgHandler({
88+
knownOptions,
89+
commandName: "list",
90+
});
91+
4992
// Parse command-line arguments
5093
const parsed = parseArgs(remainingArgs, {
51-
string: ["config", "include", "exclude", "selector", "env"],
52-
boolean: [
53-
"help",
54-
"json",
55-
"no-env",
56-
"reload",
57-
"quiet",
58-
"verbose",
59-
"debug",
60-
],
61-
collect: ["include", "exclude", "selector"],
62-
alias: {
63-
h: "help",
64-
s: "selector",
65-
r: "reload",
66-
v: "verbose",
67-
q: "quiet",
68-
d: "debug",
69-
},
70-
default: {
71-
include: undefined,
72-
exclude: undefined,
73-
selector: undefined,
74-
},
94+
...PARSE_ARGS_CONFIG,
95+
unknown: unknownHandler,
7596
});
7697

98+
// Check for unknown arguments before showing help
99+
if (unknownResult.hasErrors) {
100+
for (const unknown of unknownResult.unknownArgs) {
101+
console.error(
102+
formatUnknownArgError(unknown, {
103+
knownOptions,
104+
commandName: "list",
105+
}),
106+
);
107+
}
108+
return EXIT_CODE.USAGE_ERROR;
109+
}
110+
77111
// Show help if requested
78112
if (parsed.help) {
79113
try {

0 commit comments

Comments
 (0)