microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2137-privacy-planner

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/evals/Invoke-ContentModeration.Tests.ps1

327lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 $script:ScriptPath = (Resolve-Path (Join-Path $PSScriptRoot '../../evals/Invoke-ContentModeration.ps1')).Path
7 $script:ModulePath = (Resolve-Path (Join-Path $PSScriptRoot '../../evals/Modules/ModerationRunner.psm1')).Path
8 Import-Module $script:ModulePath -Force
9
10 # Runs the script in a child pwsh process so that explicit `exit N` calls
11 # set the process exit code. The call operator does not propagate a script
12 # file's exit code, so the child relays $LASTEXITCODE explicitly. Returns the
13 # child exit code. All output streams are suppressed to keep test output clean.
14 function Invoke-Script {
15 param([string]$Arguments)
16 $command = "& '$script:ScriptPath' $Arguments; exit `$LASTEXITCODE"
17 pwsh -NoProfile -NonInteractive -Command $command *> $null
18 return $LASTEXITCODE
19 }
20
21 # Runs the script in a child pwsh process with a fake `python` function that
22 # shadows the external interpreter. The fake function writes a canned
23 # moderate.py output file (unless -SkipOutput) and sets the simulated process
24 # exit code via $global:LASTEXITCODE. This exercises the python-in-PATH happy
25 # path and the non-zero moderate.py exit handling without invoking torch.
26 function Invoke-ScriptWithFakePython {
27 param(
28 [string]$Arguments,
29 [int]$FlaggedCount = 0,
30 [int]$PythonExitCode = 0,
31 [switch]$SkipOutput
32 )
33
34 $recordItem = if ($FlaggedCount -gt 0) {
35 "@{ id = 'rec1'; flagged = `$true; flaggedLabels = @('toxicity') }"
36 }
37 else {
38 ''
39 }
40
41 if ($SkipOutput) {
42 $writeBlock = ''
43 }
44 else {
45 $writeBlock = @"
46 `$outPath = `$null
47 for (`$i = 0; `$i -lt `$args.Count; `$i++) {
48 if (`$args[`$i] -eq '--output') { `$outPath = `$args[`$i + 1] }
49 }
50 @{ records = @($recordItem); summary = @{ total = 1; flaggedCount = $FlaggedCount } } |
51 ConvertTo-Json -Depth 10 | Set-Content -Path `$outPath -Encoding utf8NoBOM
52"@
53 }
54
55 $fakePython = @"
56function python {
57$writeBlock
58 `$global:LASTEXITCODE = $PythonExitCode
59}
60"@
61
62 $command = "$fakePython`n& '$script:ScriptPath' $Arguments; exit `$LASTEXITCODE"
63 pwsh -NoProfile -NonInteractive -Command $command *> $null
64 return $LASTEXITCODE
65 }
66}
67
68Describe 'Invoke-ContentModeration exit codes' -Tag 'Unit' {
69 Context 'when -FileList and -Records are both provided' {
70 It 'Exits with code 2' {
71 $code = Invoke-Script "-Scope 'test' -FileList 'a.md' -Records @(@{ id = 'x'; text = 'y' })"
72 $code | Should -Be 2
73 }
74 }
75
76 Context 'when neither -FileList nor -Records is provided' {
77 It 'Exits with code 2' {
78 $code = Invoke-Script "-Scope 'test'"
79 $code | Should -Be 2
80 }
81 }
82
83 Context 'when -Records is an empty array' {
84 BeforeAll {
85 $script:OutFile = Join-Path $TestDrive 'moderation-empty.json'
86 $script:Code = Invoke-Script "-Scope 'empty' -Records @() -OutFile '$script:OutFile'"
87 }
88
89 It 'Exits with code 0' {
90 $script:Code | Should -Be 0
91 }
92
93 It 'Writes an empty output file' {
94 Test-Path $script:OutFile | Should -BeTrue
95 }
96
97 It 'Reports zero total records in the output' {
98 $output = Get-Content -Path $script:OutFile -Raw | ConvertFrom-Json
99 $output.summary.total | Should -Be 0
100 $output.summary.flaggedCount | Should -Be 0
101 }
102 }
103}
104
105Describe 'Invoke-ContentModeration moderate.py invocation' -Tag 'Unit' {
106 Context 'when python is available and content passes' {
107 BeforeAll {
108 $script:OutFile = Join-Path $TestDrive 'moderation-pass.json'
109 $script:Code = Invoke-ScriptWithFakePython -Arguments "-Scope 'pass' -Records @(@{ id = 'x'; text = 'hello' }) -OutFile '$script:OutFile' -RepoRoot '$TestDrive'" -FlaggedCount 0 -PythonExitCode 0
110 }
111
112 It 'Exits with code 0' {
113 $script:Code | Should -Be 0
114 }
115 }
116
117 Context 'when python is available and content is flagged' {
118 BeforeAll {
119 $script:OutFile = Join-Path $TestDrive 'moderation-flagged.json'
120 $script:Code = Invoke-ScriptWithFakePython -Arguments "-Scope 'flag' -Records @(@{ id = 'x'; text = 'bad' }) -OutFile '$script:OutFile' -RepoRoot '$TestDrive'" -FlaggedCount 1 -PythonExitCode 0
121 }
122
123 It 'Exits with code 1' {
124 $script:Code | Should -Be 1
125 }
126 }
127
128 Context 'when moderate.py exits non-zero but output shows flagged content' {
129 BeforeAll {
130 $script:OutFile = Join-Path $TestDrive 'moderation-err-flagged.json'
131 $script:Code = Invoke-ScriptWithFakePython -Arguments "-Scope 'errflag' -Records @(@{ id = 'x'; text = 'bad' }) -OutFile '$script:OutFile' -RepoRoot '$TestDrive'" -FlaggedCount 1 -PythonExitCode 2
132 }
133
134 It 'Exits with code 1' {
135 $script:Code | Should -Be 1
136 }
137 }
138
139 Context 'when moderate.py exits non-zero with clean output' {
140 BeforeAll {
141 $script:OutFile = Join-Path $TestDrive 'moderation-err-clean.json'
142 $script:Code = Invoke-ScriptWithFakePython -Arguments "-Scope 'errclean' -Records @(@{ id = 'x'; text = 'ok' }) -OutFile '$script:OutFile' -RepoRoot '$TestDrive'" -FlaggedCount 0 -PythonExitCode 2
143 }
144
145 It 'Propagates the moderate.py exit code' {
146 $script:Code | Should -Be 2
147 }
148 }
149
150 Context 'when moderate.py exits non-zero and writes no output' {
151 BeforeAll {
152 $script:OutFile = Join-Path $TestDrive 'moderation-err-missing.json'
153 $script:Code = Invoke-ScriptWithFakePython -Arguments "-Scope 'errmissing' -Records @(@{ id = 'x'; text = 'ok' }) -OutFile '$script:OutFile' -RepoRoot '$TestDrive'" -PythonExitCode 2 -SkipOutput
154 }
155
156 It 'Propagates the moderate.py exit code' {
157 $script:Code | Should -Be 2
158 }
159 }
160}
161
162Describe 'New-ModerationInputFile' -Tag 'Unit' {
163 Context 'when given records' {
164 BeforeAll {
165 $script:Records = @(
166 @{ id = 'rec1'; text = 'Hello world' }
167 @{ id = 'rec2'; text = 'Test content' }
168 )
169 $script:InputFile = Join-Path $TestDrive 'input.jsonl'
170 $script:Result = New-ModerationInputFile -Records $script:Records -OutFile $script:InputFile
171 }
172
173 It 'Returns the output file path' {
174 $script:Result | Should -Be $script:InputFile
175 }
176
177 It 'Writes one JSON line per record' {
178 $lines = Get-Content -Path $script:InputFile
179 $lines | Should -HaveCount 2
180 }
181
182 It 'Writes valid JSON with id and text on each line' {
183 $lines = Get-Content -Path $script:InputFile
184 foreach ($line in $lines) {
185 $record = $line | ConvertFrom-Json
186 $record.id | Should -Not -BeNullOrEmpty
187 $record.text | Should -Not -BeNullOrEmpty
188 }
189 }
190 }
191
192 Context 'when no output file is specified' {
193 It 'Creates a temp file and returns its path' {
194 $records = @(@{ id = 'rec1'; text = 'content' })
195 $path = New-ModerationInputFile -Records $records
196 try {
197 Test-Path $path | Should -BeTrue
198 }
199 finally {
200 Remove-Item $path -Force -ErrorAction SilentlyContinue
201 }
202 }
203 }
204}
205
206Describe 'ConvertTo-ModerationRecords' -Tag 'Unit' {
207 Context 'when given existing files' {
208 BeforeAll {
209 $script:FileA = Join-Path $TestDrive 'file-a.md'
210 $script:FileB = Join-Path $TestDrive 'file-b.md'
211 Set-Content -Path $script:FileA -Value 'Content A' -Encoding utf8NoBOM
212 Set-Content -Path $script:FileB -Value 'Content B' -Encoding utf8NoBOM
213 $script:Records = ConvertTo-ModerationRecords -FileList @($script:FileA, $script:FileB) -RepoRoot $TestDrive
214 }
215
216 It 'Returns one record per file' {
217 $script:Records | Should -HaveCount 2
218 }
219
220 It 'Populates the text from file content' {
221 $script:Records[0].text | Should -Match 'Content A'
222 $script:Records[1].text | Should -Match 'Content B'
223 }
224
225 It 'Populates a relative id for each record' {
226 $script:Records | ForEach-Object {
227 $_.id | Should -Not -BeNullOrEmpty
228 }
229 }
230 }
231
232 Context 'when a file does not exist' {
233 It 'Skips the missing file and warns' {
234 Mock Write-Warning {} -ModuleName ModerationRunner
235 $missing = Join-Path $TestDrive 'does-not-exist.md'
236 $records = ConvertTo-ModerationRecords -FileList @($missing) -RepoRoot $TestDrive
237 $records | Should -HaveCount 0
238 Should -Invoke Write-Warning -ModuleName ModerationRunner -Times 1 -Exactly
239 }
240 }
241
242 Context 'when a file contains unicode content' {
243 It 'Preserves unicode characters in the record text' {
244 $unicodeFile = Join-Path $TestDrive 'unicode.md'
245 Set-Content -Path $unicodeFile -Value 'Café ☕ 日本語 Ñoño — emoji 🎉' -Encoding utf8NoBOM
246 $records = @(ConvertTo-ModerationRecords -FileList @($unicodeFile) -RepoRoot $TestDrive)
247 $records | Should -HaveCount 1
248 $records[0].text | Should -Match 'Café'
249 $records[0].text | Should -Match '日本語'
250 $records[0].text | Should -Match '🎉'
251 }
252 }
253
254 Context 'when a file is large' {
255 It 'Returns a single record containing the full content' {
256 $largeFile = Join-Path $TestDrive 'large.md'
257 $content = ('The quick brown fox jumps over the lazy dog. ' * 5000)
258 Set-Content -Path $largeFile -Value $content -Encoding utf8NoBOM
259 $records = @(ConvertTo-ModerationRecords -FileList @($largeFile) -RepoRoot $TestDrive)
260 $records | Should -HaveCount 1
261 $records[0].text.Length | Should -BeGreaterThan 100000
262 }
263 }
264
265 Context 'when a file contains only whitespace' {
266 It 'Returns a record without warning' {
267 Mock Write-Warning {} -ModuleName ModerationRunner
268 $wsFile = Join-Path $TestDrive 'whitespace.md'
269 Set-Content -Path $wsFile -Value " `n`t `n" -Encoding utf8NoBOM
270 $records = ConvertTo-ModerationRecords -FileList @($wsFile) -RepoRoot $TestDrive
271 $records | Should -HaveCount 1
272 Should -Invoke Write-Warning -ModuleName ModerationRunner -Times 0 -Exactly
273 }
274 }
275
276 Context 'when a file is empty' {
277 It 'Returns a record without warning' {
278 Mock Write-Warning {} -ModuleName ModerationRunner
279 $emptyFile = Join-Path $TestDrive 'empty.md'
280 [System.IO.File]::WriteAllText($emptyFile, '')
281 $records = ConvertTo-ModerationRecords -FileList @($emptyFile) -RepoRoot $TestDrive
282 $records | Should -HaveCount 1
283 Should -Invoke Write-Warning -ModuleName ModerationRunner -Times 0 -Exactly
284 }
285 }
286}
287
288Describe 'Test-ModerationOutput' -Tag 'Unit' {
289 BeforeAll {
290 Mock Write-Host {} -ModuleName ModerationRunner
291 Mock Write-Warning {} -ModuleName ModerationRunner
292 Mock Write-Error {} -ModuleName ModerationRunner
293 }
294
295 Context 'when no records are flagged' {
296 It 'Returns false' {
297 $outputPath = Join-Path $TestDrive 'clean.json'
298 @{ records = @(); summary = @{ total = 3; flaggedCount = 0 } } |
299 ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath -Encoding utf8NoBOM
300 Test-ModerationOutput -OutputPath $outputPath | Should -BeFalse
301 }
302 }
303
304 Context 'when records are flagged' {
305 It 'Returns true' {
306 $outputPath = Join-Path $TestDrive 'flagged.json'
307 @{
308 records = @(
309 @{ id = 'rec1'; flagged = $true; flaggedLabels = @('toxicity') }
310 )
311 summary = @{ total = 1; flaggedCount = 1 }
312 } | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath -Encoding utf8NoBOM
313 Test-ModerationOutput -OutputPath $outputPath | Should -BeTrue
314 }
315 }
316
317 Context 'when the output file is missing' {
318 It 'Returns true' {
319 $missing = Join-Path $TestDrive 'missing-output.json'
320 Test-ModerationOutput -OutputPath $missing | Should -BeTrue
321 }
322 }
323}
324
325AfterAll {
326 Remove-Module ModerationRunner -Force -ErrorAction SilentlyContinue
327}
328