| ... | ... | @@ -154,6 +154,48 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { |
| 154 | 154 | }), |
| 155 | 155 | ); |
| 156 | 156 | |
| 157 | it.effect("recovers colon-bearing descriptions with invocation metadata intact", () => |
| 158 | Effect.gen(function* () { |
| 159 | const fs = yield* FileSystem.FileSystem; |
| 160 | const path = yield* Path.Path; |
| 161 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); |
| 162 | const configDir = path.join(tempDir, "claude-home"); |
| 163 | |
| 164 | for (const [description, comment] of [ |
| 165 | ["Browser automation + AI test authoring via kane-cli: run browser objectives, ...", ""], |
| 166 | ['Read C:\\skills\\guide#tag: continue with "quoted".', " # trailing: comment"], |
| 167 | ] as const) { |
| 168 | yield* writeSkill( |
| 169 | path.join(configDir, "skills"), |
| 170 | "kane-cli", |
| 171 | [ |
| 172 | "---", |
| 173 | "name: frontmatter-alias", |
| 174 | `description: ${description}${comment}`, |
| 175 | "allowed-tools: [Read, Write]", |
| 176 | "disable-model-invocation: yes", |
| 177 | "user-invocable: no", |
| 178 | "---", |
| 179 | ].join("\n"), |
| 180 | ); |
| 181 | |
| 182 | const skills = yield* discoverClaudeSkills({ homePath: configDir }, undefined); |
| 183 | |
| 184 | assert.deepEqual(skills, [ |
| 185 | { |
| 186 | name: "kane-cli", |
| 187 | path: path.join(configDir, "skills", "kane-cli", "SKILL.md"), |
| 188 | enabled: true, |
| 189 | scope: "user", |
| 190 | description, |
| 191 | userInvocationOnly: true, |
| 192 | userInvocable: false, |
| 193 | }, |
| 194 | ]); |
| 195 | } |
| 196 | }), |
| 197 | ); |
| 198 | |
| 157 | 199 | it.effect("falls back to the directory name and skips malformed frontmatter", () => |
| 158 | 200 | Effect.gen(function* () { |
| 159 | 201 | const fs = yield* FileSystem.FileSystem; |
| ... | ... | @@ -163,7 +205,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { |
| 163 | 205 | const skillsDir = path.join(configDir, "skills"); |
| 164 | 206 | |
| 165 | 207 | yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n"); |
| 166 | | yield* writeSkill(skillsDir, "broken-yaml", "---\nname: [unclosed\n---\n"); |
| 208 | for (const [directoryName, field] of [ |
| 209 | ["broken-yaml", "name: [unclosed"], |
| 210 | ["broken-tools", "allowed-tools: [Read, Write"], |
| 211 | ["broken-quoted", 'name: "unclosed: text'], |
| 212 | ] as const) { |
| 213 | yield* writeSkill( |
| 214 | skillsDir, |
| 215 | directoryName, |
| 216 | ["---", "description: Run: browser objectives.", field, "---"].join("\n"), |
| 217 | ); |
| 218 | } |
| 167 | 219 | // A stray file (not a directory with SKILL.md) must be skipped. |
| 168 | 220 | yield* fs.makeDirectory(skillsDir, { recursive: true }); |
| 169 | 221 | yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill"); |