Skip to content

Commit 51b8fb9

Browse files
committed
feat(#212): enhance tests for Nova Agentic Copilot scaffold
- add tests for missing built module file - add tests for empty function definitions in built module - improve error handling in manifest schema validation - introduce case-insensitive directory policy matching - refactor warning choice prompt to accept custom UI
1 parent bca2576 commit 51b8fb9

13 files changed

Lines changed: 275 additions & 61 deletions

src/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.ps1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ function Get-NovaAgenticCopilotScaffoldWarningMessage {
2121
function Read-NovaAgenticCopilotScaffoldWarningChoice {
2222
[CmdletBinding()]
2323
param(
24-
[Parameter(Mandatory)][string]$Message
24+
[Parameter(Mandatory)][string]$Message,
25+
[Parameter()][object]$HostUi = $Host.UI
2526
)
2627

2728
$choices = [System.Management.Automation.Host.ChoiceDescription[]]@(
2829
[System.Management.Automation.Host.ChoiceDescription]::new('&Yes', 'Apply the scaffold.'),
2930
[System.Management.Automation.Host.ChoiceDescription]::new('&No', 'Cancel the operation.')
3031
)
3132

32-
return $Host.UI.PromptForChoice('Confirm', $Message, $choices, 1)
33+
return $HostUi.PromptForChoice('Confirm', $Message, $choices, 1)
3334
}
3435

3536
function Confirm-NovaAgenticCopilotScaffoldWarning {

tests/private/build/manifest/AssertManifestSchema.Tests.ps1

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ BeforeAll {
22
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)))
33
. (Join-Path $projectRoot 'src/private/build/manifest/AssertManifestSchema.ps1')
44

5-
function Stop-NovaOperation {param([string]$Message, [string]$ErrorId, $Category, $TargetObject)
6-
throw $Message
5+
function Stop-NovaOperation {
6+
param([string]$Message, [string]$ErrorId, $Category, $TargetObject)
7+
8+
$exception = [System.Exception]::new($Message)
9+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
10+
throw $record
711
}
812
}
913

@@ -13,8 +17,22 @@ Describe 'Assert-ManifestSchema' {
1317
Should -Not -Throw
1418
}
1519

16-
It 'throws when the manifest contains unknown keys' {
17-
{Assert-ManifestSchema -Manifest @{Author = 'me'; Bogus = 1} -AllowedParameter @('Author')} |
18-
Should -Throw
20+
It 'throws a sorted error when the manifest contains unknown keys' {
21+
$thrown = $null
22+
23+
try {
24+
Assert-ManifestSchema -Manifest @{
25+
Zebra = 1
26+
Author = 'me'
27+
Alpha = 2
28+
} -AllowedParameter @('Author')
29+
} catch {
30+
$thrown = $_
31+
}
32+
33+
$thrown | Should -Not -BeNullOrEmpty
34+
$thrown.FullyQualifiedErrorId | Should -Be 'Nova.Configuration.ManifestUnknownParameter'
35+
$thrown.Exception.Message | Should -Be 'Unknown parameter(s) in Manifest: Alpha, Zebra'
36+
@($thrown.TargetObject) | Should -Be @('Alpha', 'Zebra')
1937
}
2038
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function ConvertFrom-NovaCopilotCliArgument {
2+
param([string[]]$Arguments)
3+
return @{Path = '/tmp/repo'; ShortName = 'NMT'}
4+
}
5+
6+
function Invoke-NovaAgenticCopilotScaffold {
7+
param($Path, $ShortName, $Verbose, $WhatIf)
8+
return [pscustomobject]@{
9+
Path = $Path
10+
ShortName = $ShortName
11+
Verbose = [bool]$Verbose
12+
WhatIf = [bool]$WhatIf
13+
}
14+
}

tests/private/cli/InvokeNovaCliCopilotCommand.Tests.ps1

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,8 @@
11
BeforeAll {
22
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
3+
. (Join-Path $projectRoot 'src/private/cli/GetNovaCliArgumentRoutingState.ps1')
34
. (Join-Path $projectRoot 'src/private/cli/InvokeNovaCliCopilotCommand.ps1')
4-
5-
function ConvertFrom-NovaCopilotCliArgument {
6-
param([string[]]$Arguments) return @{Path = '/tmp/repo'; ShortName = 'NMT'}
7-
}
8-
function Merge-NovaCliParameterSet {
9-
param([hashtable]$BaseParameters, [hashtable]$AdditionalParameters)
10-
foreach ($name in $AdditionalParameters.Keys) {
11-
$BaseParameters[$name] = $AdditionalParameters[$name]
12-
}
13-
return $BaseParameters
14-
}
15-
function Invoke-NovaAgenticCopilotScaffold {
16-
param($Path, $ShortName, $Verbose, $WhatIf)
17-
return [pscustomobject]@{
18-
Path = $Path
19-
ShortName = $ShortName
20-
Verbose = [bool]$Verbose
21-
WhatIf = [bool]$WhatIf
22-
}
23-
}
5+
. (Join-Path $PSScriptRoot 'InvokeNovaCliCopilotCommand.TestSupport.ps1')
246
}
257

268
Describe 'Invoke-NovaCliCopilotCommand' {

tests/private/quality/duplicates/AssertBuiltModuleHasNoDuplicateFunctionNames.Tests.ps1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ function Get-Beta {}
4141
{ Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $projectInfo } | Should -Not -Throw
4242
}
4343

44+
It 'throws when the built module file does not exist' {
45+
$missingPath = Join-Path $script:projectRoot 'dist/NovaModuleTools/NovaModuleTools.psm1'
46+
$projectInfo = [pscustomobject]@{
47+
ModuleFilePSM1 = $missingPath
48+
}
49+
50+
{Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $projectInfo} |
51+
Should -Throw -ErrorId 'Nova.Environment.BuiltModuleFileNotFound'
52+
}
53+
54+
It 'throws when the built module does not define any functions' {
55+
$projectInfo = New-DuplicateValidationProjectInfo -ProjectRoot $script:projectRoot -ModuleContent '$value = 1' -SourceFileMap @{
56+
'src/public/GetAlpha.ps1' = 'function Get-Alpha {}'
57+
}
58+
59+
{Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $projectInfo} |
60+
Should -Throw -ErrorId 'Nova.Workflow.BuiltModuleFunctionListEmpty'
61+
}
62+
4463
It 'throws a source-mapped error when the built module contains duplicate top-level function names' {
4564
$projectInfo = New-DuplicateValidationProjectInfo -ProjectRoot $script:projectRoot -ModuleContent @'
4665
function Get-Alpha {}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
BeforeAll {
2+
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)))
3+
. (Join-Path $projectRoot 'src/private/quality/duplicates/GetDuplicateFunctionSourceLine.ps1')
4+
. (Join-Path $projectRoot 'src/private/quality/duplicates/FormatDuplicateFunctionErrorMessage.ps1')
5+
6+
function New-TestDuplicateOccurrence {
7+
param(
8+
[Parameter(Mandatory)][string]$Name,
9+
[Parameter(Mandatory)][int]$Line
10+
)
11+
12+
return [pscustomobject]@{
13+
Name = $Name
14+
Extent = [pscustomobject]@{
15+
StartLineNumber = $Line
16+
}
17+
}
18+
}
19+
}
20+
21+
Describe 'Format-DuplicateFunctionErrorMessage' {
22+
It 'formats duplicate groups, dist lines, and source locations in sorted order' {
23+
$duplicateGroup = @(
24+
[pscustomobject]@{
25+
Name = 'Get-Zeta'
26+
Group = @(
27+
New-TestDuplicateOccurrence -Name 'Get-Zeta' -Line 12
28+
New-TestDuplicateOccurrence -Name 'Get-Zeta' -Line 4
29+
)
30+
}
31+
[pscustomobject]@{
32+
Name = 'Get-Alpha'
33+
Group = @(
34+
New-TestDuplicateOccurrence -Name 'Get-Alpha' -Line 8
35+
)
36+
}
37+
)
38+
$sourceIndex = @{
39+
'Get-Zeta' = @(
40+
[pscustomobject]@{Path = 'src/public/GetZeta.ps1'; Line = 9}
41+
[pscustomobject]@{Path = 'src/private/GetZeta.ps1'; Line = 2}
42+
)
43+
'Get-Alpha' = @(
44+
[pscustomobject]@{Path = 'src/public/GetAlpha.ps1'; Line = 5}
45+
)
46+
}
47+
48+
$message = Format-DuplicateFunctionErrorMessage -Psm1Path '/repo/dist/NovaModuleTools.psm1' -DuplicateGroup $duplicateGroup -SourceIndex $sourceIndex
49+
$lines = $message -split "`n"
50+
51+
$lines | Should -Be @(
52+
'Duplicate top-level function names detected in built module: /repo/dist/NovaModuleTools.psm1'
53+
''
54+
'- Get-Alpha'
55+
' - dist line 8'
56+
' - source files:'
57+
' - src/public/GetAlpha.ps1:5'
58+
''
59+
'- Get-Zeta'
60+
' - dist line 4'
61+
' - dist line 12'
62+
' - source files:'
63+
' - src/private/GetZeta.ps1:2'
64+
' - src/public/GetZeta.ps1:9'
65+
)
66+
}
67+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
BeforeAll {
2+
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)))
3+
. (Join-Path $projectRoot 'src/private/quality/duplicates/GetDuplicateFunctionSourceLine.ps1')
4+
}
5+
6+
Describe 'Get-DuplicateFunctionSourceLine' {
7+
It 'returns an empty array when the source index is unavailable' {
8+
@(Get-DuplicateFunctionSourceLine -Key 'Get-Alpha').Count | Should -Be 0
9+
}
10+
11+
It 'returns an empty array when the source index does not contain the key' {
12+
@(Get-DuplicateFunctionSourceLine -Key 'Get-Alpha' -SourceIndex @{Other = @()}).Count | Should -Be 0
13+
}
14+
15+
It 'returns a sorted source list with a heading when the key exists' {
16+
$sourceIndex = @{
17+
'Get-Alpha' = @(
18+
[pscustomobject]@{Path = 'src/public/GetAlpha.ps1'; Line = 8}
19+
[pscustomobject]@{Path = 'src/private/GetAlpha.ps1'; Line = 3}
20+
[pscustomobject]@{Path = 'src/private/GetAlpha.ps1'; Line = 1}
21+
)
22+
}
23+
24+
$result = Get-DuplicateFunctionSourceLine -Key 'Get-Alpha' -SourceIndex $sourceIndex
25+
26+
$result | Should -Be @(
27+
' - source files:'
28+
' - src/private/GetAlpha.ps1:1'
29+
' - src/private/GetAlpha.ps1:3'
30+
' - src/public/GetAlpha.ps1:8'
31+
)
32+
}
33+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
BeforeAll {
2+
$projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)))
3+
. (Join-Path $projectRoot 'src/private/quality/duplicates/GetTopLevelFunctionAst.ps1')
4+
5+
function Get-TestAst {
6+
param([Parameter(Mandatory)][string]$ScriptText)
7+
8+
$tokens = $null
9+
$errors = $null
10+
return [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [ref]$tokens, [ref]$errors)
11+
}
12+
}
13+
14+
Describe 'Get-TopLevelFunctionAst' {
15+
It 'returns only top-level functions when nested functions are present' {
16+
$ast = Get-TestAst -ScriptText @'
17+
function Get-Outer {
18+
function Get-Nested {
19+
}
20+
21+
Get-Nested
22+
}
23+
24+
function Get-Second {
25+
}
26+
'@
27+
28+
$result = Get-TopLevelFunctionAst -Ast $ast
29+
30+
@($result.Name) | Should -Be @('Get-Outer', 'Get-Second')
31+
}
32+
33+
It 'returns an empty collection when the script has no functions' {
34+
$ast = Get-TestAst -ScriptText '$value = 1'
35+
36+
@(Get-TopLevelFunctionAst -Ast $ast).Count | Should -Be 0
37+
}
38+
}

tests/private/scaffold/InitializeNovaModuleAgenticCopilotScaffold.Tests.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ Describe 'Initialize-NovaModuleAgenticCopilotScaffold' {
161161
ConvertTo-NovaModuleAgenticCopilotNormalizedFileContent -Content "line1`r`nline2`r`n`r`n" | Should -Be "line1`r`nline2`r`n"
162162
}
163163

164+
It 'matches directory policy entries as case-insensitive prefixes' {
165+
Test-NovaModuleAgenticCopilotPathMatchesPolicyEntry -RelativePath 'tests/private/My.Tests.ps1' -Entry 'tests/' | Should -BeTrue
166+
Test-NovaModuleAgenticCopilotPathMatchesPolicyEntry -RelativePath 'Tests/private/My.Tests.ps1' -Entry 'tests/' | Should -BeTrue
167+
Test-NovaModuleAgenticCopilotPathMatchesPolicyEntry -RelativePath 'docs/README.md' -Entry 'tests/' | Should -BeFalse
168+
}
169+
164170
It 'overwrites managed files and preserves add-only files when a scaffold policy is supplied' {
165171
$templateRoot = Join-Path $TestDrive 'policy-template'
166172
$projectRoot = Join-Path $TestDrive 'policy-project'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
function Initialize-NovaModuleAgenticCopilotScaffold {
2+
param(
3+
[hashtable]$Answer,
4+
[string]$ProjectRoot,
5+
[switch]$Example,
6+
[AllowNull()][pscustomobject]$ScaffoldPolicy
7+
)
8+
}
9+
10+
function Stop-NovaOperation {
11+
param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject)
12+
$exception = [System.Exception]::new($Message)
13+
$record = [System.Management.Automation.ErrorRecord]::new($exception, $ErrorId, $Category, $TargetObject)
14+
throw $record
15+
}

0 commit comments

Comments
 (0)