@@ -13,7 +13,13 @@ import {
1313} from "@optique/core/message" ;
1414import { multiple , optional , withDefault } from "@optique/core/modifiers" ;
1515import { getDocPage , parse } from "@optique/core/parser" ;
16- import { argument , command , constant , option } from "@optique/core/primitives" ;
16+ import {
17+ argument ,
18+ command ,
19+ constant ,
20+ flag ,
21+ option ,
22+ } from "@optique/core/primitives" ;
1723import { integer , string } from "@optique/core/valueparser" ;
1824import assert from "node:assert/strict" ;
1925import { describe , it } from "node:test" ;
@@ -1825,3 +1831,74 @@ describe("merge() should propagate brief/description/footer from inner parsers",
18251831 ) ;
18261832 } ) ;
18271833} ) ;
1834+
1835+ describe ( "getDocPage regression: meta commands with withDefault(or(...))" , ( ) => {
1836+ // Regression test for https://github.com/dahlia/optique/issues/121
1837+ // Meta commands were missing from the command list when the user parser
1838+ // included withDefault(or(...)), because getDocPage's do...while loop
1839+ // ran the parser once even with an empty buffer, causing longestMatch to
1840+ // record the user parser as "selected" and skip all other parsers in
1841+ // getDocFragments.
1842+ it ( "should include all commands when longestMatch wraps a parser with withDefault(or(...))" , ( ) => {
1843+ // Reproduce the issue: a user parser where withDefault(or(...)) allows
1844+ // the merge to succeed with zero consumed tokens.
1845+ const configOption = withDefault (
1846+ or (
1847+ object ( { ignoreConfig : flag ( "--ignore-config" ) } ) ,
1848+ object ( { configPath : option ( "--config" , string ( { metavar : "PATH" } ) ) } ) ,
1849+ ) ,
1850+ { ignoreConfig : false , configPath : undefined } as {
1851+ readonly ignoreConfig : boolean ;
1852+ readonly configPath : string | undefined ;
1853+ } ,
1854+ ) ;
1855+
1856+ const userParser = merge (
1857+ or (
1858+ command ( "foo" , object ( { } ) , { description : message `foo cmd` } ) ,
1859+ command ( "bar" , object ( { } ) , { description : message `bar cmd` } ) ,
1860+ ) ,
1861+ configOption ,
1862+ ) ;
1863+
1864+ // Simulate what run() does: combine the user parser with meta commands
1865+ // via longestMatch.
1866+ const helpCmd = command ( "help" , object ( { } ) ) ;
1867+ const versionCmd = command ( "version" , object ( { } ) ) ;
1868+ const combined = longestMatch ( helpCmd , versionCmd , userParser ) ;
1869+
1870+ // Root-level help: getDocPage called with empty args (no subcommand selected).
1871+ const doc = getDocPage ( combined , [ ] ) ;
1872+ assert . ok ( doc , "doc should not be undefined" ) ;
1873+
1874+ const allEntries = doc . sections . flatMap ( ( s ) => s . entries ) ;
1875+ const commandNames = allEntries
1876+ . filter ( ( e ) => e . term . type === "command" )
1877+ . map ( ( e ) => ( e . term . type === "command" ? e . term . name : "" ) ) ;
1878+
1879+ assert . ok (
1880+ commandNames . includes ( "help" ) ,
1881+ `"help" should appear in the command list, got: [${
1882+ commandNames . join ( ", " )
1883+ } ]`,
1884+ ) ;
1885+ assert . ok (
1886+ commandNames . includes ( "version" ) ,
1887+ `"version" should appear in the command list, got: [${
1888+ commandNames . join ( ", " )
1889+ } ]`,
1890+ ) ;
1891+ assert . ok (
1892+ commandNames . includes ( "foo" ) ,
1893+ `"foo" should appear in the command list, got: [${
1894+ commandNames . join ( ", " )
1895+ } ]`,
1896+ ) ;
1897+ assert . ok (
1898+ commandNames . includes ( "bar" ) ,
1899+ `"bar" should appear in the command list, got: [${
1900+ commandNames . join ( ", " )
1901+ } ]`,
1902+ ) ;
1903+ } ) ;
1904+ } ) ;
0 commit comments