@@ -330,6 +330,267 @@ test "global flag keeps parent argv over child env fallback" {
330330 )
331331}
332332
333+ ///|
334+ test " default subcommand dispatches through normal child parsing " {
335+ let tui = @argparse.Command (
336+ "tui",
337+ about="Start interactive UI ",
338+ flags=[FlagArg ("trace", short='t')],
339+ options=[OptionArg ("theme", long="theme")],
340+ positionals=[PositionArg ("workspace")],
341+ )
342+ let mcp = @argparse.Command ("mcp", about="Run MCP server", options=[
343+ OptionArg ("port", long="port"),
344+ ])
345+ let cmd = @argparse.Command (
346+ "openseek",
347+ options=[OptionArg ("config", long="config", global=true )],
348+ flags=[FlagArg ("verbose", short='v', action=Count , global=true )],
349+ subcommands=[tui, mcp],
350+ default_subcommand="tui",
351+ )
352+
353+ let bare = cmd.parse(argv=[] , env=empty_env()) catch { _ => panic() }
354+ assert_true(bare.subcommand is Some (("tui", _)))
355+
356+ let defaulted = cmd.parse(
357+ argv=["--config", "config.toml", "--theme", "dark", "workspace"],
358+ env=empty_env(),
359+ ) catch {
360+ _ => panic()
361+ }
362+ assert_true(defaulted.values is { "config": ["config.toml"], .. })
363+ assert_true(
364+ defaulted.subcommand is Some (("tui", sub)) &&
365+ sub.values
366+ is {
367+ "config": ["config.toml"],
368+ "theme": ["dark"],
369+ "workspace": ["workspace"],
370+ ..
371+ },
372+ )
373+
374+ let short_global = cmd.parse(argv=["-v", "--theme", "dark"], env=empty_env()) catch {
375+ _ => panic()
376+ }
377+ assert_true(short_global.flag_counts is { "verbose": 1, .. })
378+ assert_true(
379+ short_global.subcommand is Some (("tui", sub)) &&
380+ sub.values is { "theme": ["dark"], .. } &&
381+ sub.flag_counts is { "verbose": 1, .. },
382+ )
383+
384+ let mixed_short = cmd.parse(argv=["-vt"], env=empty_env()) catch {
385+ _ => panic()
386+ }
387+ assert_true(mixed_short.flag_counts is { "verbose": 1, .. })
388+ assert_true(
389+ mixed_short.subcommand is Some (("tui", sub)) &&
390+ sub.flags is { "trace": true , .. } &&
391+ sub.flag_counts is { "verbose": 1, .. },
392+ )
393+
394+ let explicit = cmd.parse(argv=["mcp", "--port", "9000"], env=empty_env()) catch {
395+ _ => panic()
396+ }
397+ assert_true(
398+ explicit.subcommand is Some (("mcp", sub)) &&
399+ sub.values is { "port": ["9000"], .. },
400+ )
401+ }
402+
403+ ///|
404+ test " default subcommand gives exact subcommands precedence over positionals " {
405+ let cmd = @argparse.Command (
406+ "openseek",
407+ subcommands=[
408+ Command ("tui", positionals=[PositionArg ("workspace")]),
409+ Command ("mcp"),
410+ ],
411+ default_subcommand="tui",
412+ )
413+
414+ let explicit = cmd.parse(argv=["mcp"], env=empty_env()) catch { _ => panic() }
415+ assert_true(explicit.subcommand is Some (("mcp", _)))
416+
417+ let positional = cmd.parse(argv=["project"], env=empty_env()) catch {
418+ _ => panic()
419+ }
420+ assert_true(
421+ positional.subcommand is Some (("tui", sub)) &&
422+ sub.values is { "workspace": ["project"], .. },
423+ )
424+
425+ let explicit_default = cmd.parse(argv=["tui", "mcp"], env=empty_env()) catch {
426+ _ => panic()
427+ }
428+ assert_true(
429+ explicit_default.subcommand is Some (("tui", sub)) &&
430+ sub.values is { "workspace": ["mcp"], .. },
431+ )
432+
433+ let after_dash_dash = cmd.parse(argv=["--", "mcp"], env=empty_env()) catch {
434+ _ => panic()
435+ }
436+ assert_true(
437+ after_dash_dash.subcommand is Some (("tui", sub)) &&
438+ sub.values is { "workspace": ["mcp"], .. },
439+ )
440+ }
441+
442+ ///|
443+ test " default subcommand help annotation and child error context " {
444+ let cmd = @argparse.Command (
445+ "openseek",
446+ options=[OptionArg ("config", long="config", about="config", global=true )],
447+ subcommands=[
448+ Command ("tui", about="Start interactive UI ", options=[
449+ OptionArg ("theme", long="theme", about="theme"),
450+ ]),
451+ Command ("mcp", about="Run MCP server"),
452+ ],
453+ default_subcommand="tui",
454+ )
455+
456+ inspect(
457+ cmd.render_help(),
458+ content=(
459+ #|Usage : openseek [options] [command]
460+ #|
461+ #|Commands :
462+ #| tui Start interactive UI (default)
463+ #| mcp Run MCP server
464+ #| help Print help for the subcommand(s).
465+ #|
466+ #|Options :
467+ #| -h, --help Show help information.
468+ #| --config <config> config
469+ #|
470+ ),
471+ )
472+
473+ try cmd.parse(argv=["--unknown"], env=empty_env()) catch {
474+ err =>
475+ inspect(
476+ err,
477+ content=(
478+ #|error: unexpected argument '--unknown' found
479+ #|
480+ #|Usage : openseek tui [options]
481+ #|
482+ #|Start interactive UI
483+ #|
484+ #|Options :
485+ #| -h, --help Show help information.
486+ #| --config <config> config
487+ #| --theme <theme> theme
488+ #|
489+ ),
490+ )
491+ } noraise {
492+ _ => panic()
493+ }
494+ }
495+
496+ ///|
497+ test " default subcommand validation rejects ambiguous root configuration " {
498+ try
499+ @argparse.Command (
500+ "demo",
501+ subcommands=[Command ("run")],
502+ default_subcommand="missing",
503+ ).parse(argv=[] , env=empty_env())
504+ catch {
505+ err =>
506+ inspect(
507+ err,
508+ content=(
509+ #|error: command definition validation failed: default_subcommand must name a visible subcommand: missing
510+ ),
511+ )
512+ } noraise {
513+ _ => panic()
514+ }
515+
516+ try
517+ @argparse.Command (
518+ "demo",
519+ subcommands=[Command ("run")],
520+ subcommand_required=true ,
521+ default_subcommand="run",
522+ ).parse(argv=[] , env=empty_env())
523+ catch {
524+ err =>
525+ inspect(
526+ err,
527+ content=(
528+ #|error: command definition validation failed: default_subcommand cannot be used with subcommand_required
529+ ),
530+ )
531+ } noraise {
532+ _ => panic()
533+ }
534+
535+ try
536+ @argparse.Command (
537+ "demo",
538+ subcommands=[Command ("run")],
539+ arg_required_else_help=true ,
540+ default_subcommand="run",
541+ ).parse(argv=[] , env=empty_env())
542+ catch {
543+ err =>
544+ inspect(
545+ err,
546+ content=(
547+ #|error: command definition validation failed: default_subcommand cannot be used with arg_required_else_help
548+ ),
549+ )
550+ } noraise {
551+ _ => panic()
552+ }
553+
554+ try
555+ @argparse.Command (
556+ "demo",
557+ options=[OptionArg ("mode", long="mode")],
558+ subcommands=[Command ("run")],
559+ default_subcommand="run",
560+ ).parse(argv=[] , env=empty_env())
561+ catch {
562+ err =>
563+ inspect(
564+ err,
565+ content=(
566+ #|error: command definition validation failed: default_subcommand only supports global root flags/options
567+ ),
568+ )
569+ } noraise {
570+ _ => panic()
571+ }
572+
573+ try
574+ @argparse.Command (
575+ "demo",
576+ flags=[FlagArg ("verbose", long="verbose", global=true )],
577+ groups=[ArgGroup ("verbosity", args=["verbose"])],
578+ subcommands=[Command ("run")],
579+ default_subcommand="run",
580+ ).parse(argv=[] , env=empty_env())
581+ catch {
582+ err =>
583+ inspect(
584+ err,
585+ content=(
586+ #|error: command definition validation failed: default_subcommand does not support root groups
587+ ),
588+ )
589+ } noraise {
590+ _ => panic()
591+ }
592+ }
593+
333594///|
334595test " subcommand cannot follow positional arguments " {
335596 let cmd = @argparse.Command ("demo", positionals=[PositionArg ("input")], subcommands=[
0 commit comments