@@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
1212import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js" ;
1313import type { CodexComputerUseStatus } from "./app-server/computer-use.js" ;
1414import type { CodexAppServerStartOptions } from "./app-server/config.js" ;
15+ import type { JsonValue } from "./app-server/protocol.js" ;
1516import {
1617 readRecentCodexRateLimits ,
1718 resetCodexRateLimitCacheForTests ,
@@ -912,6 +913,62 @@ describe("codex command", () => {
912913 expect ( result . text ) . not . toContain ( "@here" ) ;
913914 } ) ;
914915
916+ it ( "summarizes Codex status skill groups by enabled nested skills" , async ( ) => {
917+ const deps = createDeps ( {
918+ readCodexStatusProbes : vi . fn ( async ( ) => ( {
919+ models : { ok : true as const , value : { models : [ ] } } ,
920+ account : { ok : true as const , value : { } } ,
921+ limits : { ok : true as const , value : { rateLimits : null , rateLimitsByLimitId : null } } ,
922+ mcps : { ok : true as const , value : { data : [ ] } } ,
923+ skills : {
924+ ok : true as const ,
925+ value : {
926+ data : [
927+ {
928+ cwd : "/repo-a" ,
929+ skills : [
930+ {
931+ name : "enabled-one" ,
932+ description : "" ,
933+ path : "/repo-a/.codex/skills/enabled-one/SKILL.md" ,
934+ scope : "repo" as const ,
935+ enabled : true ,
936+ } ,
937+ {
938+ name : "disabled-one" ,
939+ description : "" ,
940+ path : "/repo-a/.codex/skills/disabled-one/SKILL.md" ,
941+ scope : "repo" as const ,
942+ enabled : false ,
943+ } ,
944+ ] ,
945+ errors : [ ] ,
946+ } ,
947+ {
948+ cwd : "/repo-b" ,
949+ skills : [
950+ {
951+ name : "enabled-two" ,
952+ description : "" ,
953+ path : "/repo-b/.codex/skills/enabled-two/SKILL.md" ,
954+ scope : "repo" as const ,
955+ enabled : true ,
956+ } ,
957+ ] ,
958+ errors : [ { path : "/repo-b/bad/SKILL.md" , message : "bad skill" } ] ,
959+ } ,
960+ ] ,
961+ } ,
962+ } ,
963+ } ) ) ,
964+ } ) ;
965+
966+ const result = await handleCodexCommand ( createContext ( "status" ) , { deps } ) ;
967+
968+ expect ( result . text ) . toContain ( "Skills: 2" ) ;
969+ expect ( result . text ) . not . toContain ( "Skills: 1" ) ;
970+ } ) ;
971+
915972 it ( "summarizes generated Codex rate-limit payloads" , async ( ) => {
916973 const limits = {
917974 ok : true as const ,
@@ -3085,19 +3142,120 @@ describe("codex command", () => {
30853142 const codexControlRequest = vi
30863143 . fn ( )
30873144 . mockResolvedValueOnce ( { data : [ { name : "<@U123> [mcp](https://evil)" } ] } )
3088- . mockResolvedValueOnce ( { data : [ { id : "skill_1 @here" } ] } ) ;
3145+ . mockResolvedValueOnce ( {
3146+ data : [
3147+ {
3148+ cwd : "/repo" ,
3149+ skills : [
3150+ {
3151+ name : "skill_1 @here" ,
3152+ description : "" ,
3153+ path : "/repo/.codex/skills/skill_1/SKILL.md" ,
3154+ scope : "repo" ,
3155+ enabled : true ,
3156+ } ,
3157+ ] ,
3158+ errors : [ ] ,
3159+ } ,
3160+ ] ,
3161+ } ) ;
30893162 const deps = createDeps ( { codexControlRequest } ) ;
30903163
30913164 const mcp = await handleCodexCommand ( createContext ( "mcp" ) , { deps } ) ;
30923165 const skills = await handleCodexCommand ( createContext ( "skills" ) , { deps } ) ;
30933166
30943167 expect ( mcp . text ) . toContain ( "<\uff20U123> \uff3bmcp\uff3d\uff08https://evil\uff09" ) ;
3095- expect ( skills . text ) . toContain ( "skill\uff3f1 \uff20here" ) ;
3168+ expect ( skills . text ) . toContain ( "- ` skill\uff3f1 \uff20here` " ) ;
30963169 expect ( `${ mcp . text } \n${ skills . text } ` ) . not . toContain ( "<@U123>" ) ;
30973170 expect ( `${ mcp . text } \n${ skills . text } ` ) . not . toContain ( "[mcp](https://evil)" ) ;
30983171 expect ( `${ mcp . text } \n${ skills . text } ` ) . not . toContain ( "@here" ) ;
30993172 } ) ;
31003173
3174+ it ( "formats every Codex skill as a code-styled bullet and tolerates malformed entries" , async ( ) => {
3175+ const malformedSkillEntries : JsonValue [ ] = [
3176+ null ,
3177+ { description : "missing name" } ,
3178+ {
3179+ name : "final-skill" ,
3180+ description : "Final skill" ,
3181+ path : "/repo-b/.codex/skills/final-skill/SKILL.md" ,
3182+ scope : "repo" ,
3183+ enabled : true ,
3184+ } ,
3185+ ] ;
3186+ const codexControlRequest = vi . fn ( async ( ) => ( {
3187+ data : [
3188+ {
3189+ cwd : "/repo-a" ,
3190+ skills : Array . from ( { length : 26 } , ( _ , index ) => ( {
3191+ name : `skill-${ index + 1 } ` ,
3192+ description : `Skill ${ index + 1 } ` ,
3193+ path : `/repo-a/.codex/skills/skill-${ index + 1 } /SKILL.md` ,
3194+ scope : "repo" ,
3195+ enabled : true ,
3196+ } ) ) . concat ( {
3197+ name : "disabled-skill" ,
3198+ description : "Disabled skill" ,
3199+ path : "/repo-a/.codex/skills/disabled-skill/SKILL.md" ,
3200+ scope : "repo" ,
3201+ enabled : false ,
3202+ } ) ,
3203+ errors : [ { path : "/repo-a/bad/SKILL.md" , message : "bad skill" } ] ,
3204+ } ,
3205+ {
3206+ cwd : "/repo-b" ,
3207+ skills : malformedSkillEntries ,
3208+ errors : [ ] ,
3209+ } ,
3210+ "malformed group" ,
3211+ ] ,
3212+ } ) ) ;
3213+ const deps = createDeps ( { codexControlRequest } ) ;
3214+
3215+ const result = await handleCodexCommand ( createContext ( "skills" ) , { deps } ) ;
3216+
3217+ expect ( result . text ) . toContain ( "- `skill-1`" ) ;
3218+ expect ( result . text ) . toContain ( "- `skill-26`" ) ;
3219+ expect ( result . text ) . toContain ( "- `<unknown>`" ) ;
3220+ expect ( result . text ) . toContain ( "- `final-skill`" ) ;
3221+ expect ( result . text ) . not . toContain ( "Workspace:" ) ;
3222+ expect ( result . text ) . not . toContain ( "Error:" ) ;
3223+ expect ( result . text ) . not . toContain ( "More skills available" ) ;
3224+ expect ( result . text ) . not . toContain ( "Skill 1" ) ;
3225+ expect ( result . text ) . not . toContain ( "/repo-a/.codex/skills" ) ;
3226+ expect ( result . text ) . not . toContain ( "disabled-skill" ) ;
3227+ } ) ;
3228+
3229+ it ( "reports Codex skill load errors when no skills render" , async ( ) => {
3230+ const codexControlRequest = vi . fn ( async ( ) => ( {
3231+ data : [
3232+ {
3233+ cwd : "/repo-a" ,
3234+ skills : [
3235+ {
3236+ name : "disabled-skill" ,
3237+ description : "Disabled skill" ,
3238+ path : "/repo-a/.codex/skills/disabled-skill/SKILL.md" ,
3239+ scope : "repo" ,
3240+ enabled : false ,
3241+ } ,
3242+ ] ,
3243+ errors : [
3244+ { path : "/repo-a/bad/SKILL.md" , message : "bad skill <@U123>" } ,
3245+ { path : "/repo-a/other/SKILL.md" , message : "other bad skill @here" } ,
3246+ ] ,
3247+ } ,
3248+ ] ,
3249+ } ) ) ;
3250+ const deps = createDeps ( { codexControlRequest } ) ;
3251+
3252+ const result = await handleCodexCommand ( createContext ( "skills" ) , { deps } ) ;
3253+
3254+ expect ( result . text ) . toBe ( "Codex skills: none returned (2 load errors)." ) ;
3255+ expect ( result . text ) . not . toContain ( "<@U123>" ) ;
3256+ expect ( result . text ) . not . toContain ( "@here" ) ;
3257+ } ) ;
3258+
31013259 it ( "returns sanitized command failures instead of leaking app-server errors" , async ( ) => {
31023260 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
31033261 await fs . writeFile (
0 commit comments