11import { describe , expect , it } from "vitest" ;
2- import { formatCliParseErrorOutput } from "./error-output.js" ;
2+ import { formatCliParseErrorOutput , suggestClosestCommand } from "./error-output.js" ;
33
44describe ( "formatCliParseErrorOutput" , ( ) => {
55 it ( "explains unknown commands with root help and plugin hints" , ( ) => {
@@ -12,6 +12,43 @@ describe("formatCliParseErrorOutput", () => {
1212 ) ;
1313 } ) ;
1414
15+ it ( "suggests the closest root command for a typo when knownCommands is provided (#83999)" , ( ) => {
16+ const output = formatCliParseErrorOutput ( "error: unknown command 'upate'\n" , {
17+ argv : [ "node" , "openclaw" , "upate" ] ,
18+ knownCommands : [ "update" , "doctor" , "channels" , "plugins" ] ,
19+ } ) ;
20+
21+ expect ( output ) . toBe (
22+ 'OpenClaw does not know the command "upate".\nDid you mean this?\n openclaw update\nTry: openclaw --help\nPlugin command? openclaw plugins list\nDocs: https://docs.openclaw.ai/cli\n' ,
23+ ) ;
24+ } ) ;
25+
26+ it ( "surfaces the explicit alias for `upgrade` -> `update` (#83999)" , ( ) => {
27+ const output = formatCliParseErrorOutput ( "error: unknown command 'upgrade'\n" , {
28+ argv : [ "node" , "openclaw" , "upgrade" ] ,
29+ knownCommands : [ "update" , "doctor" ] ,
30+ } ) ;
31+
32+ expect ( output ) . toContain ( "Did you mean this?\n openclaw update" ) ;
33+ } ) ;
34+
35+ it ( "does not suggest when the typo is far from every known command (#83999)" , ( ) => {
36+ const output = formatCliParseErrorOutput ( "error: unknown command 'zzqwerty'\n" , {
37+ argv : [ "node" , "openclaw" , "zzqwerty" ] ,
38+ knownCommands : [ "update" , "doctor" , "channels" ] ,
39+ } ) ;
40+
41+ expect ( output ) . not . toContain ( "Did you mean this?" ) ;
42+ } ) ;
43+
44+ it ( "is a no-op when knownCommands is not provided (legacy caller shape)" , ( ) => {
45+ const output = formatCliParseErrorOutput ( "error: unknown command 'upate'\n" , {
46+ argv : [ "node" , "openclaw" , "upate" ] ,
47+ } ) ;
48+
49+ expect ( output ) . not . toContain ( "Did you mean this?" ) ;
50+ } ) ;
51+
1552 it ( "points unknown options at the active command help" , ( ) => {
1653 const output = formatCliParseErrorOutput ( "error: unknown option '--wat'\n" , {
1754 argv : [ "node" , "openclaw" , "channels" , "status" , "--wat" ] ,
@@ -32,3 +69,34 @@ describe("formatCliParseErrorOutput", () => {
3269 ) ;
3370 } ) ;
3471} ) ;
72+
73+ describe ( "suggestClosestCommand (#83999)" , ( ) => {
74+ const known = [ "update" , "doctor" , "channels" , "plugins" , "agents" , "status" ] ;
75+
76+ it ( "picks the canonical update on `upate`" , ( ) => {
77+ expect ( suggestClosestCommand ( "upate" , known ) ) . toBe ( "update" ) ;
78+ } ) ;
79+
80+ it ( "returns undefined when nothing is close enough" , ( ) => {
81+ expect ( suggestClosestCommand ( "zzqwerty" , known ) ) . toBeUndefined ( ) ;
82+ } ) ;
83+
84+ it ( "honors the explicit upgrade -> update alias even when edit distance picks another" , ( ) => {
85+ // `upgrade` distance to `update` is 2 (`gr` -> `t`) — without the alias map
86+ // the Levenshtein-only path could ambiguously land on another short root.
87+ expect ( suggestClosestCommand ( "upgrade" , known ) ) . toBe ( "update" ) ;
88+ } ) ;
89+
90+ it ( "does not invent a command — alias target must be in knownCommands" , ( ) => {
91+ expect ( suggestClosestCommand ( "upgrade" , [ "doctor" , "channels" ] ) ) . toBeUndefined ( ) ;
92+ } ) ;
93+
94+ it ( "returns undefined for empty input" , ( ) => {
95+ expect ( suggestClosestCommand ( "" , known ) ) . toBeUndefined ( ) ;
96+ expect ( suggestClosestCommand ( " " , known ) ) . toBeUndefined ( ) ;
97+ } ) ;
98+
99+ it ( "matches case-insensitively against known commands" , ( ) => {
100+ expect ( suggestClosestCommand ( "DocTr" , known ) ) . toBe ( "doctor" ) ;
101+ } ) ;
102+ } ) ;
0 commit comments