@rover-studio/answer-me 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bin/answerme-toolkit.mjs +6 -0
  2. package/distribution/npm/migrations.json +605 -0
  3. package/distribution/npm/package-manifest.json +192 -0
  4. package/distribution/npm/skills/answerme/SKILL.md +94 -0
  5. package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
  6. package/distribution/npm/skills/answerme/references/api.md +135 -0
  7. package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
  8. package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
  9. package/distribution/npm/skills/answerme/references/errors.md +44 -0
  10. package/distribution/npm/skills/answerme/references/handoff.md +46 -0
  11. package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
  12. package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
  13. package/distribution/npm/skills/answerme/references/templates.md +139 -0
  14. package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
  15. package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
  16. package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
  17. package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
  18. package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
  19. package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
  20. package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
  21. package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
  22. package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
  23. package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
  24. package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
  25. package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
  26. package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
  27. package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
  28. package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
  29. package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
  30. package/lib/npm-cli/commands.mjs +247 -0
  31. package/lib/npm-cli/constants.mjs +51 -0
  32. package/lib/npm-cli/errors.mjs +15 -0
  33. package/lib/npm-cli/filesystem.mjs +193 -0
  34. package/lib/npm-cli/host-discovery.mjs +404 -0
  35. package/lib/npm-cli/main.mjs +42 -0
  36. package/lib/npm-cli/package-integrity.mjs +212 -0
  37. package/lib/npm-cli/transaction.mjs +375 -0
  38. package/lib/npm-cli/usage-validation.mjs +349 -0
  39. package/package.json +17 -0
@@ -0,0 +1,498 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$ResumeInteractionId,
4
+ [string]$ApiBaseUrl,
5
+ [switch]$AllowInsecureLoopbackHttpForTest,
6
+ [string]$CredentialPath = $env:ANSWERME_CREATOR_CREDENTIAL_PATH,
7
+ [string]$ResultTokenStoreRoot = $env:ANSWERME_RESULT_TOKEN_STORE_ROOT,
8
+ [switch]$AllowNonDefaultResultTokenStore,
9
+ [object]$WaitTimeoutSeconds = 300,
10
+ [object]$EnrollmentWaitTimeoutSeconds = 300,
11
+ [ValidateRange(1, 60)]
12
+ [int]$PollSeconds = 5,
13
+ [switch]$SkipPopup,
14
+ [switch]$ValidateOnly
15
+ )
16
+
17
+ $ErrorActionPreference = 'Stop'
18
+
19
+ $processStartInfoSupport = Join-Path $PSScriptRoot 'windows-process-start-info.ps1'
20
+ if (-not (Test-Path -LiteralPath $processStartInfoSupport -PathType Leaf)) {
21
+ throw 'The AnswerMe Windows process argument helper is missing.'
22
+ }
23
+ . $processStartInfoSupport
24
+
25
+ function Write-AnswerMeSelfTestIssue {
26
+ param(
27
+ [Parameter(Mandatory = $true)][string]$Stage,
28
+ [Parameter(Mandatory = $true)][string]$Code,
29
+ [Parameter(Mandatory = $true)][string]$Message,
30
+ [Parameter(Mandatory = $true)][int]$ExitCode,
31
+ [bool]$NetworkCalled = $false,
32
+ [AllowNull()][object]$Cleanup
33
+ )
34
+
35
+ if ($null -eq $Cleanup) {
36
+ $Cleanup = [PSCustomObject]@{ attempted = $false; removed = $false }
37
+ }
38
+ [PSCustomObject]@{
39
+ ok = $false
40
+ status = 'issue'
41
+ stage = $Stage
42
+ code = $Code
43
+ message = $Message
44
+ networkCalled = $NetworkCalled
45
+ resultTokenCleanup = $Cleanup
46
+ cleanupWarning = $null
47
+ } | ConvertTo-Json -Depth 6 -Compress
48
+ exit $ExitCode
49
+ }
50
+
51
+ function Invoke-AnswerMeSelfTestChild {
52
+ param(
53
+ [Parameter(Mandatory = $true)][string]$ScriptPath,
54
+ [Parameter(Mandatory = $true)][object[]]$Arguments,
55
+ [switch]$AllowEnrollmentEvent,
56
+ [AllowNull()][Uri]$ResolvedApiBaseUrl
57
+ )
58
+
59
+ $pwshPath = (Get-Process -Id $PID).Path
60
+ $startInfo = [Diagnostics.ProcessStartInfo]::new()
61
+ $startInfo.FileName = $pwshPath
62
+ $startInfo.UseShellExecute = $false
63
+ $startInfo.CreateNoWindow = $true
64
+ $startInfo.RedirectStandardOutput = $true
65
+ $startInfo.RedirectStandardError = $true
66
+ $startInfo.RedirectStandardInput = $true
67
+ $acknowledgementToken = $null
68
+ if ($AllowEnrollmentEvent) {
69
+ $acknowledgementToken = [Guid]::NewGuid().ToString('N')
70
+ Set-AnswerMeProcessEnvironmentVariable `
71
+ -StartInfo $startInfo `
72
+ -Name 'ANSWERME_INTERNAL_SELFTEST_CREATE_EVENT_ACK' `
73
+ -Value $acknowledgementToken
74
+ }
75
+ Set-AnswerMeProcessArguments `
76
+ -StartInfo $startInfo `
77
+ -Arguments (@('-NoLogo', '-NoProfile', '-File', $ScriptPath) + $Arguments)
78
+
79
+ $process = [Diagnostics.Process]::new()
80
+ $process.StartInfo = $startInfo
81
+ try {
82
+ if (-not $process.Start()) { throw 'Self-test child process could not be started.' }
83
+ $stdoutTask = $process.StandardOutput.ReadToEndAsync()
84
+ $invalidStderr = $false
85
+ $enrollmentEventCount = 0
86
+ while ($null -ne ($line = $process.StandardError.ReadLine())) {
87
+ if ($AllowEnrollmentEvent -and
88
+ $enrollmentEventCount -eq 0 -and
89
+ $null -ne $ResolvedApiBaseUrl -and
90
+ (Test-AnswerMeSelfTestEnrollmentEvent `
91
+ -Line $line `
92
+ -ResolvedApiBaseUrl $ResolvedApiBaseUrl)) {
93
+ $enrollmentEventCount += 1
94
+ [Console]::Error.WriteLine($line)
95
+ [Console]::Error.Flush()
96
+ $process.StandardInput.WriteLine($acknowledgementToken)
97
+ $process.StandardInput.Flush()
98
+ }
99
+ else {
100
+ $invalidStderr = $true
101
+ if ($AllowEnrollmentEvent) {
102
+ $process.StandardInput.WriteLine('invalid-event')
103
+ $process.StandardInput.Flush()
104
+ }
105
+ }
106
+ }
107
+ $process.WaitForExit()
108
+ $text = $stdoutTask.GetAwaiter().GetResult().Trim()
109
+ $body = $null
110
+ if (-not [string]::IsNullOrWhiteSpace($text)) {
111
+ try {
112
+ $body = $text | ConvertFrom-Json
113
+ }
114
+ catch {
115
+ $body = $null
116
+ }
117
+ }
118
+ return [PSCustomObject]@{
119
+ exitCode = $process.ExitCode
120
+ body = $body
121
+ invalidStderr = $invalidStderr
122
+ enrollmentEventCount = $enrollmentEventCount
123
+ }
124
+ }
125
+ finally {
126
+ $process.Dispose()
127
+ }
128
+ }
129
+
130
+ function Test-AnswerMeSelfTestResponseFields {
131
+ param(
132
+ [AllowNull()][object]$Body,
133
+ [Parameter(Mandatory = $true)][string[]]$ExpectedFields
134
+ )
135
+
136
+ if ($Body -isnot [PSCustomObject]) { return $false }
137
+ $actual = @($Body.PSObject.Properties.Name | Sort-Object)
138
+ $expected = @($ExpectedFields | Sort-Object)
139
+ return $actual.Count -eq $expected.Count -and
140
+ -not (Compare-Object -ReferenceObject $expected -DifferenceObject $actual -CaseSensitive)
141
+ }
142
+
143
+ function Test-AnswerMeSelfTestEnrollmentEvent {
144
+ param(
145
+ [Parameter(Mandatory = $true)][string]$Line,
146
+ [Parameter(Mandatory = $true)][Uri]$ResolvedApiBaseUrl
147
+ )
148
+
149
+ try {
150
+ $eventBody = $Line | ConvertFrom-Json
151
+ }
152
+ catch {
153
+ return $false
154
+ }
155
+ if (-not (Test-AnswerMeSelfTestResponseFields `
156
+ -Body $eventBody `
157
+ -ExpectedFields @('event', 'stage', 'verificationUrl')) -or
158
+ $eventBody.event -isnot [string] -or
159
+ [string]$eventBody.event -cne 'answerme.creator-enrollment.verification-url' -or
160
+ $eventBody.stage -isnot [string] -or
161
+ [string]$eventBody.stage -cne 'verification-handoff' -or
162
+ $eventBody.verificationUrl -isnot [string]) {
163
+ return $false
164
+ }
165
+
166
+ $eventUri = $null
167
+ return [Uri]::TryCreate([string]$eventBody.verificationUrl, [UriKind]::Absolute, [ref]$eventUri) -and
168
+ $eventUri.Scheme.Equals($ResolvedApiBaseUrl.Scheme, [StringComparison]::OrdinalIgnoreCase) -and
169
+ $eventUri.DnsSafeHost.Equals($ResolvedApiBaseUrl.DnsSafeHost, [StringComparison]::OrdinalIgnoreCase) -and
170
+ $eventUri.Port -eq $ResolvedApiBaseUrl.Port -and
171
+ [string]::IsNullOrEmpty($eventUri.UserInfo) -and
172
+ [string]::IsNullOrEmpty($eventUri.Fragment)
173
+ }
174
+
175
+ function Get-AnswerMeSelfTestExpectedApiBaseUrl {
176
+ if (-not (Get-Command Resolve-AnswerMeApiBaseUrl -ErrorAction SilentlyContinue)) {
177
+ $policyPath = Join-Path $PSScriptRoot 'answerme-api-base-url.ps1'
178
+ if (-not (Test-Path -LiteralPath $policyPath -PathType Leaf)) { return $null }
179
+ try { . $policyPath } catch { return $null }
180
+ }
181
+
182
+ $candidate = $ApiBaseUrl
183
+ $credential = $null
184
+ if ([string]::IsNullOrWhiteSpace($candidate)) {
185
+ $candidateCredentialPath = $CredentialPath
186
+ if ([string]::IsNullOrWhiteSpace($candidateCredentialPath) -and
187
+ [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) {
188
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
189
+ if (-not [string]::IsNullOrWhiteSpace($localAppData)) {
190
+ $candidateCredentialPath = Join-Path $localAppData 'AnswerMe\creator-credential.clixml'
191
+ }
192
+ }
193
+ if (-not [string]::IsNullOrWhiteSpace($candidateCredentialPath) -and
194
+ (Test-Path -LiteralPath $candidateCredentialPath -PathType Leaf)) {
195
+ try {
196
+ $credential = Import-Clixml -LiteralPath $candidateCredentialPath
197
+ if ($credential -is [PSCredential]) { $candidate = $credential.UserName }
198
+ }
199
+ catch {
200
+ return $null
201
+ }
202
+ finally {
203
+ $credential = $null
204
+ }
205
+ }
206
+ }
207
+ if ([string]::IsNullOrWhiteSpace($candidate)) { return $null }
208
+ try {
209
+ return Resolve-AnswerMeApiBaseUrl `
210
+ -ApiBaseUrl $candidate `
211
+ -AllowInsecureLoopbackHttpForTest:$AllowInsecureLoopbackHttpForTest
212
+ }
213
+ catch {
214
+ return $null
215
+ }
216
+ }
217
+
218
+ function Get-AnswerMeSelfTestAnswer {
219
+ param(
220
+ [Parameter(Mandatory = $true)][object[]]$Answers,
221
+ [Parameter(Mandatory = $true)][string]$QuestionId,
222
+ [Parameter(Mandatory = $true)][string[]]$AllowedOptionIds
223
+ )
224
+
225
+ $matches = @($Answers | Where-Object { [string]$_.questionId -ceq $QuestionId })
226
+ if ($matches.Count -ne 1) {
227
+ throw "Self-test result must contain exactly one answer for '$QuestionId'."
228
+ }
229
+ $answer = $matches[0]
230
+ $fields = @($answer.PSObject.Properties.Name | Sort-Object)
231
+ if ($fields.Count -ne 2 -or
232
+ $fields -notcontains 'questionId' -or
233
+ $fields -notcontains 'optionId' -or
234
+ [string]$answer.optionId -cnotin $AllowedOptionIds) {
235
+ throw "Self-test result contains an invalid answer for '$QuestionId'."
236
+ }
237
+ return [string]$answer.optionId
238
+ }
239
+
240
+ $createScript = Join-Path $PSScriptRoot 'create-answerme.ps1'
241
+ $waitScript = Join-Path $PSScriptRoot 'wait-answerme-result.ps1'
242
+ $cleanupScript = Join-Path $PSScriptRoot 'remove-answerme-result-token.ps1'
243
+ foreach ($requiredScript in @($createScript, $waitScript, $cleanupScript)) {
244
+ if (-not (Test-Path -LiteralPath $requiredScript -PathType Leaf)) {
245
+ Write-AnswerMeSelfTestIssue `
246
+ -Stage 'preflight' `
247
+ -Code 'self-test-dependency-missing' `
248
+ -Message 'A required AnswerMe self-test adapter is missing.' `
249
+ -ExitCode 3
250
+ }
251
+ }
252
+
253
+ $template = [ordered]@{
254
+ schemaVersion = 1
255
+ type = 'questionnaire'
256
+ topic = 'AnswerMe 安装自检'
257
+ intro = '请分别核对页面显示、自动弹出和问题诊断建议。三项答案只用于诊断,不影响安装结果。'
258
+ questions = @(
259
+ [ordered]@{
260
+ id = 'page-display'
261
+ type = 'single-choice'
262
+ title = 'AnswerMe 答题页面是否正常显示?'
263
+ shortLabel = '页面显示'
264
+ required = $true
265
+ minSelections = 1
266
+ maxSelections = 1
267
+ options = @(
268
+ [ordered]@{ id = 'displayed'; label = '是,页面正常显示' },
269
+ [ordered]@{ id = 'not-displayed'; label = '否,页面未正常显示' }
270
+ )
271
+ allowCustomAnswer = $false
272
+ },
273
+ [ordered]@{
274
+ id = 'automatic-popup'
275
+ type = 'single-choice'
276
+ title = '答题页面是否自动弹出并置于前台?'
277
+ shortLabel = '自动弹出'
278
+ required = $true
279
+ minSelections = 1
280
+ maxSelections = 1
281
+ options = @(
282
+ [ordered]@{ id = 'foreground'; label = '是,已自动弹出并置前' },
283
+ [ordered]@{ id = 'not-foreground'; label = '否,未自动弹出或未置前' }
284
+ )
285
+ allowCustomAnswer = $false
286
+ },
287
+ [ordered]@{
288
+ id = 'diagnostic-advice'
289
+ type = 'single-choice'
290
+ title = '是否需要生成脱敏诊断报告?正常使用请选择“不生成诊断报告”。'
291
+ shortLabel = '诊断建议'
292
+ required = $true
293
+ minSelections = 1
294
+ maxSelections = 1
295
+ options = @(
296
+ [ordered]@{ id = 'do-not-recommend'; label = '不生成诊断报告' },
297
+ [ordered]@{ id = 'recommend-redacted'; label = '建议生成脱敏诊断报告' }
298
+ )
299
+ allowCustomAnswer = $false
300
+ }
301
+ )
302
+ allowAdditionalContext = $false
303
+ expiresInSeconds = 3600
304
+ visual = [ordered]@{ theme = 'auto' }
305
+ }
306
+ $templateJson = $template | ConvertTo-Json -Depth 16 -Compress
307
+
308
+ if ($PSBoundParameters.ContainsKey('ResumeInteractionId')) {
309
+ Write-AnswerMeSelfTestIssue `
310
+ -Stage 'preflight' `
311
+ -Code 'self-test-resume-unsupported' `
312
+ -Message 'Installation usage validation must create and wait for its own fixed questionnaire.' `
313
+ -ExitCode 2
314
+ }
315
+
316
+ if ($ValidateOnly) {
317
+ $createArguments = @(
318
+ '-TemplateJson', $templateJson,
319
+ '-WaitTimeoutSeconds', [string]$WaitTimeoutSeconds,
320
+ '-EnrollmentWaitTimeoutSeconds', [string]$EnrollmentWaitTimeoutSeconds,
321
+ '-ValidateOnly'
322
+ )
323
+ if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) { $createArguments += @('-ApiBaseUrl', $ApiBaseUrl) }
324
+ if (-not [string]::IsNullOrWhiteSpace($CredentialPath)) { $createArguments += @('-CredentialPath', $CredentialPath) }
325
+ if (-not [string]::IsNullOrWhiteSpace($ResultTokenStoreRoot)) { $createArguments += @('-ResultTokenStoreRoot', $ResultTokenStoreRoot) }
326
+ if ($AllowInsecureLoopbackHttpForTest) { $createArguments += '-AllowInsecureLoopbackHttpForTest' }
327
+ if ($AllowNonDefaultResultTokenStore) { $createArguments += '-AllowNonDefaultResultTokenStore' }
328
+ if ($SkipPopup) { $createArguments += '-SkipPopup' }
329
+ $create = Invoke-AnswerMeSelfTestChild -ScriptPath $createScript -Arguments $createArguments
330
+ if ($create.invalidStderr) {
331
+ Write-AnswerMeSelfTestIssue `
332
+ -Stage 'create' `
333
+ -Code 'self-test-create-stderr-invalid' `
334
+ -Message 'The AnswerMe create adapter emitted an untrusted stderr record.' `
335
+ -ExitCode 4 `
336
+ -NetworkCalled $false
337
+ }
338
+ if ($create.exitCode -ne 0 -or $null -eq $create.body -or $create.body.ok -ne $true) {
339
+ Write-AnswerMeSelfTestIssue `
340
+ -Stage 'create' `
341
+ -Code 'self-test-create-validation-failed' `
342
+ -Message 'The AnswerMe self-test questionnaire contract could not be validated.' `
343
+ -ExitCode 4 `
344
+ -NetworkCalled $false
345
+ }
346
+ [PSCustomObject]@{
347
+ ok = $true
348
+ status = 'validated'
349
+ stage = 'preflight'
350
+ networkCalled = $false
351
+ resultTokenCleanup = [PSCustomObject]@{ attempted = $false; removed = $false }
352
+ cleanupWarning = $null
353
+ } | ConvertTo-Json -Depth 4 -Compress
354
+ exit 0
355
+ }
356
+
357
+ $interactionId = $null
358
+ $createArguments = @(
359
+ '-TemplateJson', $templateJson,
360
+ '-WaitTimeoutSeconds', [string]$WaitTimeoutSeconds,
361
+ '-EnrollmentWaitTimeoutSeconds', [string]$EnrollmentWaitTimeoutSeconds
362
+ )
363
+ if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) { $createArguments += @('-ApiBaseUrl', $ApiBaseUrl) }
364
+ if (-not [string]::IsNullOrWhiteSpace($CredentialPath)) { $createArguments += @('-CredentialPath', $CredentialPath) }
365
+ if (-not [string]::IsNullOrWhiteSpace($ResultTokenStoreRoot)) { $createArguments += @('-ResultTokenStoreRoot', $ResultTokenStoreRoot) }
366
+ if ($AllowInsecureLoopbackHttpForTest) { $createArguments += '-AllowInsecureLoopbackHttpForTest' }
367
+ if ($AllowNonDefaultResultTokenStore) { $createArguments += '-AllowNonDefaultResultTokenStore' }
368
+ if ($SkipPopup) { $createArguments += '-SkipPopup' }
369
+
370
+ $expectedEnrollmentApiBaseUrl = Get-AnswerMeSelfTestExpectedApiBaseUrl
371
+ $create = Invoke-AnswerMeSelfTestChild `
372
+ -ScriptPath $createScript `
373
+ -Arguments $createArguments `
374
+ -AllowEnrollmentEvent `
375
+ -ResolvedApiBaseUrl $expectedEnrollmentApiBaseUrl
376
+ if ($create.invalidStderr) {
377
+ Write-AnswerMeSelfTestIssue `
378
+ -Stage 'create' `
379
+ -Code 'self-test-create-stderr-invalid' `
380
+ -Message 'The AnswerMe create adapter emitted an untrusted stderr record.' `
381
+ -ExitCode 4 `
382
+ -NetworkCalled ($null -ne $create.body -and $create.body.networkCalled -eq $true)
383
+ }
384
+ if ($create.exitCode -ne 0 -or
385
+ $null -eq $create.body -or
386
+ $create.body.ok -ne $true -or
387
+ [string]$create.body.status -cne 'pending' -or
388
+ $create.body.resultTokenPersisted -ne $true -or
389
+ $create.body.questionCount -ne 3 -or
390
+ $create.body.interactionId -isnot [string] -or
391
+ [string]::IsNullOrWhiteSpace([string]$create.body.interactionId) -or
392
+ ([string]$create.body.interactionId).Length -gt 100) {
393
+ Write-AnswerMeSelfTestIssue `
394
+ -Stage 'create' `
395
+ -Code 'self-test-create-failed' `
396
+ -Message 'The AnswerMe self-test questionnaire could not be created and verified.' `
397
+ -ExitCode 4 `
398
+ -NetworkCalled ($null -ne $create.body -and $create.body.networkCalled -eq $true)
399
+ }
400
+
401
+ $interactionId = [string]$create.body.interactionId
402
+
403
+ $waitArguments = @(
404
+ '-InteractionId', $interactionId,
405
+ '-TimeoutSeconds', [string]$WaitTimeoutSeconds,
406
+ '-PollSeconds', [string]$PollSeconds
407
+ )
408
+ if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
409
+ $waitArguments += @('-ApiBaseUrl', $ApiBaseUrl)
410
+ }
411
+ if (-not [string]::IsNullOrWhiteSpace($CredentialPath)) {
412
+ $waitArguments += @('-CredentialPath', $CredentialPath)
413
+ }
414
+ if (-not [string]::IsNullOrWhiteSpace($ResultTokenStoreRoot)) {
415
+ $waitArguments += @('-ResultTokenStoreRoot', $ResultTokenStoreRoot)
416
+ }
417
+ if ($AllowInsecureLoopbackHttpForTest) { $waitArguments += '-AllowInsecureLoopbackHttpForTest' }
418
+ if ($AllowNonDefaultResultTokenStore) { $waitArguments += '-AllowNonDefaultResultTokenStore' }
419
+
420
+ $wait = Invoke-AnswerMeSelfTestChild -ScriptPath $waitScript -Arguments $waitArguments
421
+ if ($wait.invalidStderr -or $wait.exitCode -ne 0 -or $null -eq $wait.body -or $wait.body.ok -ne $true) {
422
+ Write-AnswerMeSelfTestIssue `
423
+ -Stage 'wait' `
424
+ -Code 'self-test-wait-failed' `
425
+ -Message 'The AnswerMe self-test did not reach a readable terminal result.' `
426
+ -ExitCode 4 `
427
+ -NetworkCalled $true
428
+ }
429
+
430
+ $terminal = $wait.body.response
431
+ try {
432
+ if ($null -eq $terminal -or
433
+ [string]$terminal.interactionId -cne $interactionId -or
434
+ [string]$terminal.status -cne 'submitted' -or
435
+ $null -eq $terminal.result -or
436
+ [string]$terminal.result.type -cne 'questionnaire') {
437
+ throw 'Self-test terminal result is not the original submitted questionnaire.'
438
+ }
439
+ $answers = @($terminal.result.answers)
440
+ if ($answers.Count -ne 3) {
441
+ throw 'Self-test result must contain exactly three answers.'
442
+ }
443
+ [void](Get-AnswerMeSelfTestAnswer `
444
+ -Answers $answers `
445
+ -QuestionId 'page-display' `
446
+ -AllowedOptionIds @('displayed', 'not-displayed'))
447
+ [void](Get-AnswerMeSelfTestAnswer `
448
+ -Answers $answers `
449
+ -QuestionId 'automatic-popup' `
450
+ -AllowedOptionIds @('foreground', 'not-foreground'))
451
+ [void](Get-AnswerMeSelfTestAnswer `
452
+ -Answers $answers `
453
+ -QuestionId 'diagnostic-advice' `
454
+ -AllowedOptionIds @('recommend-redacted', 'do-not-recommend'))
455
+ }
456
+ catch {
457
+ Write-AnswerMeSelfTestIssue `
458
+ -Stage 'mapping' `
459
+ -Code 'self-test-result-invalid' `
460
+ -Message 'The AnswerMe self-test result did not match the fixed three-question contract.' `
461
+ -ExitCode 4 `
462
+ -NetworkCalled $true
463
+ }
464
+
465
+ $cleanupArguments = @('-InteractionId', $interactionId)
466
+ if (-not [string]::IsNullOrWhiteSpace($ResultTokenStoreRoot)) { $cleanupArguments += @('-ResultTokenStoreRoot', $ResultTokenStoreRoot) }
467
+ if ($AllowNonDefaultResultTokenStore) { $cleanupArguments += '-AllowNonDefaultResultTokenStore' }
468
+ $cleanup = Invoke-AnswerMeSelfTestChild -ScriptPath $cleanupScript -Arguments $cleanupArguments
469
+ $cleanupSucceeded = -not $cleanup.invalidStderr -and
470
+ $cleanup.exitCode -eq 0 -and
471
+ $null -ne $cleanup.body -and
472
+ $cleanup.body.ok -eq $true -and
473
+ $cleanup.body.removed -eq $true
474
+ $cleanupEvidence = [PSCustomObject]@{
475
+ attempted = $true
476
+ removed = $cleanupSucceeded
477
+ }
478
+ $cleanupWarning = if ($cleanupSucceeded) {
479
+ $null
480
+ }
481
+ else {
482
+ [PSCustomObject]@{
483
+ code = 'self-test-cleanup-failed'
484
+ message = 'The protected self-test Result Token could not be confirmed removed.'
485
+ }
486
+ }
487
+
488
+ [PSCustomObject]@{
489
+ ok = $true
490
+ status = 'passed'
491
+ stage = 'complete'
492
+ code = $null
493
+ message = $null
494
+ networkCalled = $true
495
+ resultTokenCleanup = $cleanupEvidence
496
+ cleanupWarning = $cleanupWarning
497
+ } | ConvertTo-Json -Depth 6 -Compress
498
+ exit 0