microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/996-standardize-timestamp-skill-validation

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/plugins/PluginHelpers.Tests.ps1

462lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 Import-Module $PSScriptRoot/../../plugins/Modules/PluginHelpers.psm1 -Force
7}
8
9Describe 'New-PluginReadmeContent - maturity notice' {
10 It 'Includes experimental notice when maturity is experimental' {
11 $collection = @{
12 id = 'test-exp'
13 name = 'Test Experimental'
14 description = 'An experimental collection'
15 }
16 $items = @(@{ Name = 'test-agent'; Description = 'desc'; Kind = 'agent' })
17 $result = New-PluginReadmeContent -Collection $collection -Items $items -Maturity 'experimental'
18 $result | Should -Match '\u26A0' # warning sign emoji
19 }
20
21 It 'Has no notice when maturity is stable' {
22 $collection = @{
23 id = 'test-stable'
24 name = 'Test Stable'
25 description = 'A stable collection'
26 }
27 $items = @(@{ Name = 'test-agent'; Description = 'desc'; Kind = 'agent' })
28 $result = New-PluginReadmeContent -Collection $collection -Items $items -Maturity 'stable'
29 $result | Should -Not -Match '\u26A0'
30 }
31
32 It 'Has no notice when maturity is omitted' {
33 $collection = @{
34 id = 'test-default'
35 name = 'Test Default'
36 description = 'A default collection'
37 }
38 $items = @(@{ Name = 'test-agent'; Description = 'desc'; Kind = 'agent' })
39 $result = New-PluginReadmeContent -Collection $collection -Items $items
40 $result | Should -Not -Match '\u26A0'
41 }
42
43 It 'Has no notice when maturity is null' {
44 $collection = @{
45 id = 'test-null'
46 name = 'Test Null'
47 description = 'A null maturity collection'
48 }
49 $items = @(@{ Name = 'test-agent'; Description = 'desc'; Kind = 'agent' })
50 $result = New-PluginReadmeContent -Collection $collection -Items $items -Maturity $null
51 $result | Should -Not -Match '\u26A0'
52 }
53}
54
55Describe 'Get-PluginItemName' {
56 It 'Strips .agent.md to .md for agents' {
57 $result = Get-PluginItemName -FileName 'task-researcher.agent.md' -Kind 'agent'
58 $result | Should -Be 'task-researcher.md'
59 }
60
61 It 'Strips .prompt.md to .md for prompts' {
62 $result = Get-PluginItemName -FileName 'gen-plan.prompt.md' -Kind 'prompt'
63 $result | Should -Be 'gen-plan.md'
64 }
65
66 It 'Preserves .instructions.md suffix' {
67 $result = Get-PluginItemName -FileName 'csharp.instructions.md' -Kind 'instruction'
68 $result | Should -Be 'csharp.instructions.md'
69 }
70
71 It 'Returns skill directory name unchanged' {
72 $result = Get-PluginItemName -FileName 'video-to-gif' -Kind 'skill'
73 $result | Should -Be 'video-to-gif'
74 }
75}
76
77Describe 'Get-PluginItemSubpath' {
78 It 'Extracts single-level collection subdirectory for agents' {
79 $result = Get-PluginItemSubpath -Path '.github/agents/hve-core/rpi-agent.agent.md' -Kind 'agent'
80 $result | Should -Be 'hve-core'
81 }
82
83 It 'Extracts nested subdirectory path for agent subagents' {
84 $result = Get-PluginItemSubpath -Path '.github/agents/hve-core/subagents/researcher-subagent.agent.md' -Kind 'agent'
85 $result | Should -Be 'hve-core/subagents'
86 }
87
88 It 'Returns empty string when item is at kind root' {
89 $result = Get-PluginItemSubpath -Path '.github/agents/root-agent.agent.md' -Kind 'agent'
90 $result | Should -Be ''
91 }
92
93 It 'Extracts subdirectory for instructions' {
94 $result = Get-PluginItemSubpath -Path '.github/instructions/shared/hve-core-location.instructions.md' -Kind 'instruction'
95 $result | Should -Be 'shared'
96 }
97
98 It 'Extracts subdirectory for skills' {
99 $result = Get-PluginItemSubpath -Path '.github/skills/shared/pr-reference' -Kind 'skill'
100 $result | Should -Be 'shared'
101 }
102
103 It 'Handles backslash-separated paths' {
104 $result = Get-PluginItemSubpath -Path '.github\agents\hve-core\rpi-agent.agent.md' -Kind 'agent'
105 $result | Should -Be 'hve-core'
106 }
107
108 It 'Extracts subdirectory for prompts' {
109 $result = Get-PluginItemSubpath -Path '.github/prompts/hve-core/git-commit-message.prompt.md' -Kind 'prompt'
110 $result | Should -Be 'hve-core'
111 }
112
113 It 'Returns empty string when path does not match kind prefix' {
114 $result = Get-PluginItemSubpath -Path 'some/other/path/file.md' -Kind 'agent'
115 $result | Should -Be ''
116 }
117}
118
119Describe 'New-PluginManifestContent' {
120 It 'Returns hashtable with name, description, and version' {
121 $result = New-PluginManifestContent -CollectionId 'test-plugin' -Description 'A test plugin' -Version '2.0.0'
122 $result.name | Should -Be 'test-plugin'
123 $result.description | Should -Be 'A test plugin'
124 $result.version | Should -Be '2.0.0'
125 }
126
127 It 'Includes explicit path arrays when provided' {
128 $result = New-PluginManifestContent `
129 -CollectionId 'with-paths' -Description 'desc' -Version '1.0.0' `
130 -AgentPaths @('agents/core/') `
131 -CommandPaths @('commands/core/', 'commands/ado/') `
132 -SkillPaths @('skills/shared/')
133 $result.agents | Should -Be @('agents/core/')
134 $result.commands | Should -Be @('commands/ado/', 'commands/core/')
135 $result.skills | Should -Be @('skills/shared/')
136 }
137
138 It 'Omits component keys when no paths provided' {
139 $result = New-PluginManifestContent -CollectionId 'minimal' -Description 'desc' -Version '1.0.0'
140 $result.Contains('agents') | Should -BeFalse
141 $result.Contains('commands') | Should -BeFalse
142 $result.Contains('skills') | Should -BeFalse
143 }
144
145 It 'Returns ordered hashtable' {
146 $result = New-PluginManifestContent -CollectionId 'ordered-test' -Description 'desc' -Version '1.0.0'
147 $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary]
148 }
149}
150
151Describe 'Get-PluginSubdirectory' {
152 It 'Maps agent to agents' {
153 $result = Get-PluginSubdirectory -Kind 'agent'
154 $result | Should -Be 'agents'
155 }
156
157 It 'Maps prompt to commands' {
158 $result = Get-PluginSubdirectory -Kind 'prompt'
159 $result | Should -Be 'commands'
160 }
161
162 It 'Maps instruction to instructions' {
163 $result = Get-PluginSubdirectory -Kind 'instruction'
164 $result | Should -Be 'instructions'
165 }
166
167 It 'Maps skill to skills' {
168 $result = Get-PluginSubdirectory -Kind 'skill'
169 $result | Should -Be 'skills'
170 }
171}
172
173Describe 'Write-PluginDirectory - DryRun mode' {
174 BeforeAll {
175 $script:repoRoot = Join-Path $TestDrive 'wpd-repo'
176 $script:pluginsDir = Join-Path $TestDrive 'wpd-plugins'
177 New-Item -ItemType Directory -Path $script:repoRoot -Force | Out-Null
178 New-Item -ItemType Directory -Path $script:pluginsDir -Force | Out-Null
179
180 # Create a valid agent file with frontmatter
181 $agentDir = Join-Path $script:repoRoot '.github/agents/test'
182 New-Item -ItemType Directory -Path $agentDir -Force | Out-Null
183 Set-Content -Path (Join-Path $agentDir 'example.agent.md') -Value "---`ndescription: An example agent`n---`nAgent body"
184
185 # Create a valid skill directory with SKILL.md
186 $skillDir = Join-Path $script:repoRoot '.github/skills/test/my-skill'
187 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
188 Set-Content -Path (Join-Path $skillDir 'SKILL.md') -Value "---`ndescription: A skill`n---`nSkill body"
189
190 # Create shared dirs
191 New-Item -ItemType Directory -Path (Join-Path $script:repoRoot 'docs/templates') -Force | Out-Null
192 New-Item -ItemType Directory -Path (Join-Path $script:repoRoot 'scripts/lib') -Force | Out-Null
193 }
194
195 It 'Completes DryRun without creating files for agents' {
196 $collection = @{
197 id = 'dryrun-test'
198 name = 'DryRun Test'
199 description = 'Testing DryRun mode'
200 items = @(
201 @{
202 path = '.github/agents/test/example.agent.md'
203 kind = 'agent'
204 }
205 )
206 }
207
208 $result = Write-PluginDirectory -Collection $collection -PluginsDir $script:pluginsDir `
209 -RepoRoot $script:repoRoot -Version '1.0.0' -DryRun
210
211 $result.Success | Should -BeTrue
212 $result.AgentCount | Should -Be 1
213
214 # Verify no actual files were created
215 $pluginDir = Join-Path $script:pluginsDir 'dryrun-test'
216 Test-Path -Path $pluginDir | Should -BeFalse
217 }
218
219 It 'Includes collection subdirectory in GeneratedFiles path' {
220 $collection = @{
221 id = 'subpath-test'
222 name = 'Subpath Test'
223 description = 'Testing subpath in destination'
224 items = @(
225 @{
226 path = '.github/agents/test/example.agent.md'
227 kind = 'agent'
228 }
229 )
230 }
231
232 $result = Write-PluginDirectory -Collection $collection -PluginsDir $script:pluginsDir `
233 -RepoRoot $script:repoRoot -Version '1.0.0' -DryRun
234
235 $result.Success | Should -BeTrue
236 # GeneratedFiles should contain a path with the 'test' subdirectory preserved
237 $agentPaths = @($result.GeneratedFiles | Where-Object { $_ -match 'agents' -and $_ -match 'example' })
238 $agentPaths | Should -Not -BeNullOrEmpty
239 $agentPaths[0] | Should -Match 'agents[/\\]test[/\\]example\.md$'
240 }
241
242 It 'Completes DryRun with skill items' {
243 $collection = @{
244 id = 'dryrun-skill'
245 name = 'DryRun Skill'
246 description = 'Testing DryRun with skills'
247 items = @(
248 @{
249 path = '.github/skills/test/my-skill'
250 kind = 'skill'
251 }
252 )
253 }
254
255 $result = Write-PluginDirectory -Collection $collection -PluginsDir $script:pluginsDir `
256 -RepoRoot $script:repoRoot -Version '1.0.0' -DryRun
257
258 $result.Success | Should -BeTrue
259 $result.SkillCount | Should -Be 1
260 }
261
262 It 'Handles source file not found for non-skill items' {
263 $collection = @{
264 id = 'missing-source'
265 name = 'Missing Source'
266 description = 'Non-existent source file'
267 items = @(
268 @{
269 path = '.github/agents/test/nonexistent.agent.md'
270 kind = 'agent'
271 }
272 )
273 }
274
275 $result = Write-PluginDirectory -Collection $collection -PluginsDir $script:pluginsDir `
276 -RepoRoot $script:repoRoot -Version '1.0.0' -DryRun
277
278 $result.Success | Should -BeTrue
279 $result.AgentCount | Should -Be 1
280 }
281
282 It 'Warns when shared directory is missing' {
283 $emptyRepo = Join-Path $TestDrive 'empty-repo'
284 New-Item -ItemType Directory -Path $emptyRepo -Force | Out-Null
285
286 # Create agent file but no shared directories
287 $agentDir = Join-Path $emptyRepo '.github/agents/test'
288 New-Item -ItemType Directory -Path $agentDir -Force | Out-Null
289 Set-Content -Path (Join-Path $agentDir 'a.agent.md') -Value "---`ndescription: test`n---"
290
291 $collection = @{
292 id = 'no-shared'
293 name = 'No Shared'
294 description = 'Missing shared dirs'
295 items = @(
296 @{
297 path = '.github/agents/test/a.agent.md'
298 kind = 'agent'
299 }
300 )
301 }
302
303 $result = Write-PluginDirectory -Collection $collection -PluginsDir $script:pluginsDir `
304 -RepoRoot $emptyRepo -Version '1.0.0' -DryRun
305
306 $result.Success | Should -BeTrue
307 }
308}
309
310Describe 'Test-SymlinkCapability' {
311 It 'Returns a boolean' {
312 $result = Test-SymlinkCapability
313 $result | Should -BeOfType [bool]
314 }
315
316 It 'Cleans up probe directory' {
317 $probeDirPattern = Join-Path ([System.IO.Path]::GetTempPath()) "hve-symlink-probe-$PID"
318 Test-SymlinkCapability | Out-Null
319 Test-Path $probeDirPattern | Should -BeFalse
320 }
321}
322
323Describe 'New-PluginLink' {
324 BeforeAll {
325 $script:linkRoot = Join-Path $TestDrive 'link-test'
326 New-Item -ItemType Directory -Path $script:linkRoot -Force | Out-Null
327 }
328
329 It 'Writes text stub when SymlinkCapable is false' {
330 $src = Join-Path $script:linkRoot 'src-stub.txt'
331 Set-Content -Path $src -Value 'content' -NoNewline
332 $dest = Join-Path $script:linkRoot 'dest-stub.txt'
333
334 New-PluginLink -SourcePath $src -DestinationPath $dest
335
336 Test-Path $dest | Should -BeTrue
337 $stubContent = [System.IO.File]::ReadAllText($dest)
338 $expectedPath = [System.IO.Path]::GetRelativePath((Split-Path -Parent $dest), $src) -replace '\\', '/'
339 $stubContent | Should -Be $expectedPath
340 }
341
342 It 'Creates parent directory when destination parent does not exist' {
343 $src = Join-Path $script:linkRoot 'src-parent.txt'
344 Set-Content -Path $src -Value 'data' -NoNewline
345 $dest = Join-Path $script:linkRoot 'nested/deep/dest-parent.txt'
346
347 New-PluginLink -SourcePath $src -DestinationPath $dest
348
349 Test-Path $dest | Should -BeTrue
350 }
351}
352
353Describe 'Repair-PluginSymlinkIndex' {
354 Context 'When PluginsDir does not exist' {
355 It 'Returns 0' {
356 $result = Repair-PluginSymlinkIndex `
357 -PluginsDir (Join-Path $TestDrive 'nonexistent') `
358 -RepoRoot $TestDrive
359 $result | Should -Be 0
360 }
361 }
362
363 Context 'In a git repository with text stubs' {
364 BeforeAll {
365 $script:repoRoot = Join-Path $TestDrive 'symlink-repo'
366 New-Item -ItemType Directory -Path $script:repoRoot -Force | Out-Null
367
368 Push-Location $script:repoRoot
369 try {
370 git init --quiet 2>$null
371 git config user.email 'test@test.com'
372 git config user.name 'Test'
373
374 $script:pluginsDir = Join-Path $script:repoRoot 'plugins'
375 New-Item -ItemType Directory -Path $script:pluginsDir -Force | Out-Null
376
377 # Valid text stub: small, starts with ../, no newlines
378 [System.IO.File]::WriteAllText(
379 (Join-Path $script:pluginsDir 'valid-stub.md'),
380 '../some/source.md'
381 )
382
383 # Large file (>500 bytes) — skipped by size filter
384 [System.IO.File]::WriteAllText(
385 (Join-Path $script:pluginsDir 'large-file.md'),
386 ('x' * 501)
387 )
388
389 # Non-stub content — skipped by pattern filter
390 [System.IO.File]::WriteAllText(
391 (Join-Path $script:pluginsDir 'non-stub.md'),
392 '# Regular markdown'
393 )
394
395 # Stub with newline — skipped by newline filter
396 [System.IO.File]::WriteAllText(
397 (Join-Path $script:pluginsDir 'newline-stub.md'),
398 "../path/file.md`n"
399 )
400
401 git add -- plugins/ 2>$null
402 git commit -m 'initial' --quiet 2>$null
403 } finally {
404 Pop-Location
405 }
406 }
407
408 It 'Counts only valid stubs in DryRun mode' {
409 Push-Location $script:repoRoot
410 try {
411 $result = Repair-PluginSymlinkIndex `
412 -PluginsDir $script:pluginsDir `
413 -RepoRoot $script:repoRoot -DryRun
414 $result | Should -Be 1
415 } finally {
416 Pop-Location
417 }
418 }
419
420 It 'Does not modify index in DryRun mode' {
421 Push-Location $script:repoRoot
422 try {
423 $before = git ls-files --stage -- plugins/valid-stub.md 2>$null
424 Repair-PluginSymlinkIndex `
425 -PluginsDir $script:pluginsDir `
426 -RepoRoot $script:repoRoot -DryRun | Out-Null
427 $after = git ls-files --stage -- plugins/valid-stub.md 2>$null
428 $before | Should -Be $after
429 } finally {
430 Pop-Location
431 }
432 }
433
434 It 'Re-indexes tracked text stub as mode 120000' {
435 Push-Location $script:repoRoot
436 try {
437 $result = Repair-PluginSymlinkIndex `
438 -PluginsDir $script:pluginsDir `
439 -RepoRoot $script:repoRoot
440 $result | Should -Be 1
441
442 $lsOutput = git ls-files --stage -- plugins/valid-stub.md 2>$null
443 $lsOutput | Should -Match '^120000'
444 } finally {
445 Pop-Location
446 }
447 }
448
449 It 'Skips entries already at mode 120000' {
450 Push-Location $script:repoRoot
451 try {
452 # Previous test fixed the stub; second run finds nothing new
453 $result = Repair-PluginSymlinkIndex `
454 -PluginsDir $script:pluginsDir `
455 -RepoRoot $script:repoRoot
456 $result | Should -Be 0
457 } finally {
458 Pop-Location
459 }
460 }
461 }
462}
463