| ... | @@ -154,6 +154,48 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { | ... | @@ -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 | it.effect("falls back to the directory name and skips malformed frontmatter", () => | 199 | it.effect("falls back to the directory name and skips malformed frontmatter", () => |
| 158 | Effect.gen(function* () { | 200 | Effect.gen(function* () { |
| 159 | const fs = yield* FileSystem.FileSystem; | 201 | const fs = yield* FileSystem.FileSystem; |
| ... | @@ -163,7 +205,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { | ... | @@ -163,7 +205,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { |
| 163 | const skillsDir = path.join(configDir, "skills"); | 205 | const skillsDir = path.join(configDir, "skills"); |
| 164 | | 206 | |
| 165 | yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n"); | 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 | // A stray file (not a directory with SKILL.md) must be skipped. | 219 | // A stray file (not a directory with SKILL.md) must be skipped. |
| 168 | yield* fs.makeDirectory(skillsDir, { recursive: true }); | 220 | yield* fs.makeDirectory(skillsDir, { recursive: true }); |
| 169 | yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill"); | 221 | yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill"); |