1<#
2Usage: .\corpus.ps1 -Mode Create -Root C:\one-tests\runs\20260904-01
3 .\corpus.ps1 -Mode Capture -Root C:\one-tests\runs\20260904-01 -Step 08-manual-ink
4#>
5[CmdletBinding()]
6param(
7 [ValidateSet('Create', 'Capture')]
8 [string]$Mode = 'Create',
9
10 [Parameter(Mandatory = $true)]
11 [string]$Root,
12
13 [string]$Step
14)
15
16Set-StrictMode -Version Latest
17$ErrorActionPreference = 'Stop'
18
19$schema = 1
20$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
21$runs = [IO.Path]::GetFullPath('C:\one-tests\runs').TrimEnd('\')
22$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
23$markerPath = Join-Path $root '.native-corpus.json'
24$manifestPath = Join-Path $root 'manifest.json'
25$logPath = Join-Path $root 'operation-log.jsonl'
26$notebookPath = Join-Path $root 'notebook'
27$sectionName = 'synthetic.one'
28$sectionPath = Join-Path $notebookPath $sectionName
29$script:app = $null
30$script:openedNotebookId = $null
31$script:sectionId = $null
32$script:manifest = $null
33
34function Write-Json {
35 param(
36 $Value,
37 [string]$Path
38 )
39
40 $Value | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $Path -Encoding UTF8
41}
42
43function Save-Manifest {
44 if ($null -ne $script:manifest) {
45 Write-Json $script:manifest $manifestPath
46 }
47}
48
49function Add-Log {
50 param(
51 [string]$Action,
52 [string]$Fixture,
53 $Details
54 )
55
56 $entry = [pscustomobject][ordered]@{
57 at_utc = [DateTime]::UtcNow.ToString('o')
58 fixture = $Fixture
59 action = $Action
60 details = $Details
61 }
62 $entry | ConvertTo-Json -Depth 12 -Compress | Add-Content -LiteralPath $logPath -Encoding UTF8
63}
64
65function Add-Fixture {
66 param(
67 [string]$Name,
68 [string]$Status,
69 [string]$Description,
70 $Details
71 )
72
73 $script:manifest.fixtures = @($script:manifest.fixtures) + [pscustomobject][ordered]@{
74 name = $Name
75 status = $Status
76 description = $Description
77 at_utc = [DateTime]::UtcNow.ToString('o')
78 details = $Details
79 }
80 Save-Manifest
81}
82
83function Assert-RunRoot {
84 $parent = [IO.Path]::GetDirectoryName($root)
85 if (-not [String]::Equals($parent, $runs, [StringComparison]::OrdinalIgnoreCase)) {
86 throw 'Choose a new direct child of C:\one-tests\runs.'
87 }
88 if ([String]::Equals($root, $runs, [StringComparison]::OrdinalIgnoreCase)) {
89 throw 'Choose a run directory below C:\one-tests\runs.'
90 }
91
92 $oneTests = [IO.Path]::GetDirectoryName($runs)
93 if (-not (Test-Path -LiteralPath $oneTests -PathType Container)) {
94 throw 'Create C:\one-tests before running this harness.'
95 }
96 if (-not (Test-Path -LiteralPath $runs -PathType Container)) {
97 New-Item -ItemType Directory -Path $runs | Out-Null
98 }
99 if (((Get-Item -LiteralPath $runs -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
100 throw 'C:\one-tests\runs cannot be a reparse point.'
101 }
102}
103
104function Assert-PathUnderRoot {
105 param([string]$Path)
106
107 $full = [IO.Path]::GetFullPath($Path)
108 if (-not $full.StartsWith("$root\", [StringComparison]::OrdinalIgnoreCase)) {
109 throw 'The harness can write only below its run root.'
110 }
111 return $full
112}
113
114function Get-TreeState {
115 param([string]$Path)
116
117 $full = Assert-PathUnderRoot $Path
118 $files = @(Get-ChildItem -LiteralPath $full -Force -File -Recurse | Sort-Object FullName)
119 $entries = @(
120 foreach ($file in $files) {
121 $relative = $file.FullName.Substring($full.Length).TrimStart('\')
122 [pscustomobject][ordered]@{
123 path = $relative
124 bytes = [int64]$file.Length
125 sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
126 }
127 }
128 )
129 $lines = @($entries | ForEach-Object { "$($_.path)`t$($_.bytes)`t$($_.sha256)" })
130 $bytes = [Text.Encoding]::UTF8.GetBytes([String]::Join("`n", $lines))
131 $treeHash = [Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
132 [pscustomobject][ordered]@{
133 file_count = $entries.Count
134 byte_count = [int64](($entries | Measure-Object -Property bytes -Sum).Sum)
135 sha256 = ([BitConverter]::ToString($treeHash) -replace '-', '').ToLowerInvariant()
136 files = $entries
137 }
138}
139
140function Copy-Snapshot {
141 param([string]$Fixture)
142
143 $snapshot = Join-Path (Join-Path $root 'snapshots') $Fixture
144 Assert-PathUnderRoot $snapshot | Out-Null
145 if (Test-Path -LiteralPath $snapshot) {
146 throw "Snapshot '$Fixture' already exists."
147 }
148 $before = Get-TreeState $notebookPath
149 New-Item -ItemType Directory -Path $snapshot | Out-Null
150 Copy-Item -LiteralPath $notebookPath -Destination $snapshot -Recurse -Force
151 $copied = Get-TreeState (Join-Path $snapshot 'notebook')
152 $after = Get-TreeState $notebookPath
153 if ($before.sha256 -ne $copied.sha256 -or $before.sha256 -ne $after.sha256) {
154 throw "Snapshot '$Fixture' changed while copying."
155 }
156 $state = [pscustomobject][ordered]@{
157 source_before = [pscustomobject][ordered]@{ file_count = $before.file_count; byte_count = $before.byte_count; sha256 = $before.sha256 }
158 copied = [pscustomobject][ordered]@{ file_count = $copied.file_count; byte_count = $copied.byte_count; sha256 = $copied.sha256 }
159 source_after = [pscustomobject][ordered]@{ file_count = $after.file_count; byte_count = $after.byte_count; sha256 = $after.sha256 }
160 files = $copied.files
161 }
162 Write-Json $state (Join-Path $snapshot 'snapshot.json')
163 return $state
164}
165
166function Assert-NotebookPaths {
167 param([string]$Hierarchy)
168
169 [xml]$document = $Hierarchy
170 foreach ($node in @($document.SelectNodes('//*[@path]'))) {
171 $path = $node.GetAttribute('path')
172 if (-not [String]::IsNullOrWhiteSpace($path)) {
173 $full = [IO.Path]::GetFullPath($path)
174 if (-not $full.StartsWith("$notebookPath\", [StringComparison]::OrdinalIgnoreCase) -and
175 -not [String]::Equals($full.TrimEnd('\'), $notebookPath, [StringComparison]::OrdinalIgnoreCase)) {
176 throw 'OneNote returned a hierarchy path outside the generated notebook.'
177 }
178 }
179 }
180 return $document
181}
182
183function Get-PageXml {
184 param([string]$PageId)
185
186 $page = ''
187 $script:app.GetPageContent($PageId, [ref]$page, 3, $schema)
188 return $page
189}
190
191function Get-PageIds {
192 param([xml]$Hierarchy)
193
194 $manager = New-Object Xml.XmlNamespaceManager($Hierarchy.NameTable)
195 $manager.AddNamespace('one', $namespace)
196 return @($Hierarchy.SelectNodes('//one:Page', $manager) | ForEach-Object { $_.GetAttribute('ID') })
197}
198
199function Read-ReadyGeneratedNotebook {
200 param([int]$ExpectedPageCount = -1)
201
202 if ([String]::IsNullOrWhiteSpace($script:openedNotebookId) -or [String]::IsNullOrWhiteSpace($script:sectionId)) {
203 throw 'The generated notebook and section must be open before readiness is checked.'
204 }
205 $deadline = (Get-Date).AddSeconds(15)
206 $lastError = $null
207 while ((Get-Date) -lt $deadline) {
208 try {
209 $hierarchy = ''
210 $script:app.GetHierarchy($script:openedNotebookId, 4, [ref]$hierarchy, $schema)
211 $document = Assert-NotebookPaths $hierarchy
212 $manager = New-Object Xml.XmlNamespaceManager($document.NameTable)
213 $manager.AddNamespace('one', $namespace)
214 $sections = @($document.SelectNodes('//one:Section', $manager) | Where-Object {
215 [String]::Equals($_.GetAttribute('ID'), $script:sectionId, [StringComparison]::OrdinalIgnoreCase) -and
216 [String]::Equals([IO.Path]::GetFullPath($_.GetAttribute('path')).TrimEnd('\'), $sectionPath, [StringComparison]::OrdinalIgnoreCase)
217 })
218 if ($sections.Count -ne 1) { throw 'OneNote has not returned the generated section in the hierarchy.' }
219 $pageIds = @(Get-PageIds $document | Where-Object { -not [String]::IsNullOrWhiteSpace($_) })
220 if ($ExpectedPageCount -ge 0 -and $pageIds.Count -ne $ExpectedPageCount) {
221 throw "Expected $ExpectedPageCount persisted pages, found $($pageIds.Count)."
222 }
223 $pageContents = @()
224 foreach ($pageId in $pageIds) {
225 $pageContents += Get-PageXml $pageId
226 }
227 return [pscustomobject][ordered]@{
228 hierarchy = $hierarchy
229 page_ids = $pageIds
230 page_contents = $pageContents
231 }
232 }
233 catch {
234 $lastError = $_.Exception.Message
235 Start-Sleep -Milliseconds 250
236 }
237 }
238 throw "OneNote did not return the generated section ready for capture: $lastError"
239}
240
241function Get-SolePageId {
242 $state = Read-ReadyGeneratedNotebook
243 $pages = @($state.page_ids)
244 if ($pages.Count -ne 1 -or [String]::IsNullOrWhiteSpace($pages[0])) {
245 throw 'The generated notebook must contain exactly one page.'
246 }
247 return $pages[0]
248}
249
250function Confirm-Closed {
251 param([string]$NotebookId)
252
253 $probe = ''
254 $closed = $false
255 try {
256 $script:app.GetHierarchy($NotebookId, 0, [ref]$probe, $schema)
257 $closed = [String]::IsNullOrWhiteSpace($probe)
258 }
259 catch {
260 $closed = $true
261 }
262 if (-not $closed) {
263 throw 'OneNote still reports the generated notebook as open.'
264 }
265}
266
267function Write-PageEvidence {
268 param(
269 [string]$Directory,
270 [string]$Hierarchy,
271 [string[]]$PageIds,
272 [string[]]$PageContents
273 )
274
275 if ($PageIds.Count -ne $PageContents.Count) {
276 throw 'Page evidence is incomplete.'
277 }
278 Assert-PathUnderRoot $Directory | Out-Null
279 $pagesDirectory = Join-Path $Directory 'pages'
280 New-Item -ItemType Directory -Path $pagesDirectory | Out-Null
281 [IO.File]::WriteAllText((Join-Path $Directory 'hierarchy.xml'), $Hierarchy, [Text.Encoding]::UTF8)
282 $pages = @()
283 for ($index = 0; $index -lt $PageIds.Count; $index++) {
284 $pageFile = 'page-{0:d2}.xml' -f $index
285 [IO.File]::WriteAllText((Join-Path $pagesDirectory $pageFile), $PageContents[$index], [Text.Encoding]::UTF8)
286 $pages += [pscustomobject][ordered]@{ id = $PageIds[$index]; file = "pages\\$pageFile" }
287 }
288 return $pages
289}
290
291function Open-GeneratedNotebook {
292 param([int]$ExpectedPageCount = -1)
293
294 $opened = ''
295 $script:app.OpenHierarchy($notebookPath, '', [ref]$opened, 0)
296 if ([String]::IsNullOrWhiteSpace($opened)) {
297 throw 'OneNote did not open the generated notebook.'
298 }
299 $script:openedNotebookId = $opened
300 $section = ''
301 $script:app.OpenHierarchy($sectionName, $opened, [ref]$section, 0)
302 if ([String]::IsNullOrWhiteSpace($section)) {
303 throw 'OneNote did not find the generated section.'
304 }
305 $script:sectionId = $section
306 $state = Read-ReadyGeneratedNotebook $ExpectedPageCount
307 Add-Log 'open_notebook' $null @{ notebook_id = $opened; path = $notebookPath; expected_page_count = if ($ExpectedPageCount -ge 0) { $ExpectedPageCount } else { $null }; actual_page_count = $state.page_ids.Count }
308 return $state
309}
310
311function Sync-CloseGeneratedNotebook {
312 param(
313 [string]$Fixture,
314 [string]$Phase
315 )
316
317 $closingId = $script:openedNotebookId
318 if ([String]::IsNullOrWhiteSpace($closingId)) { throw 'The generated notebook must be open before it is synced.' }
319 $script:app.SyncHierarchy($closingId)
320 Add-Log 'sync_hierarchy' $Fixture @{ notebook_id = $closingId; phase = $Phase }
321 $script:app.CloseNotebook($closingId, $false)
322 $script:openedNotebookId = $null
323 Confirm-Closed $closingId
324 Add-Log 'close_notebook' $Fixture @{ notebook_id = $closingId; phase = $Phase; confirmed = $true }
325}
326
327function Capture-Notebook {
328 param([string]$Fixture)
329
330 $evidence = Join-Path (Join-Path $root 'evidence') $Fixture
331 Assert-PathUnderRoot $evidence | Out-Null
332 if (Test-Path -LiteralPath $evidence) {
333 throw "Evidence '$Fixture' already exists."
334 }
335 $baseline = Read-ReadyGeneratedNotebook
336 $expectedPageCount = $baseline.page_ids.Count
337 Sync-CloseGeneratedNotebook $Fixture 'stabilize'
338 $snapshot = Copy-Snapshot $Fixture
339 Add-Log 'copy_snapshot' $Fixture @{ sha256 = $snapshot.copied.sha256; file_count = $snapshot.copied.file_count; byte_count = $snapshot.copied.byte_count }
340 $state = Open-GeneratedNotebook $expectedPageCount
341
342 New-Item -ItemType Directory -Path $evidence | Out-Null
343 $pages = @(Write-PageEvidence $evidence $state.hierarchy $state.page_ids $state.page_contents)
344 if ($Fixture -eq '06-attachment') {
345 $cachePaths = @(
346 foreach ($pageXml in @($state.page_contents)) {
347 [xml]$pageDocument = $pageXml
348 $manager = New-Object Xml.XmlNamespaceManager($pageDocument.NameTable)
349 $manager.AddNamespace('one', $namespace)
350 foreach ($file in @($pageDocument.SelectNodes('//one:InsertedFile', $manager))) {
351 if ($file.GetAttribute('preferredName') -eq 'fictitious-attachment.txt' -and -not [String]::IsNullOrWhiteSpace($file.GetAttribute('pathCache'))) {
352 $file.GetAttribute('pathCache')
353 }
354 }
355 }
356 )
357 if ($cachePaths.Count -ne 1 -or -not (Test-Path -LiteralPath $cachePaths[0] -PathType Leaf)) {
358 throw 'OneNote did not return one readable cached generated attachment.'
359 }
360 $attachment = [IO.File]::ReadAllBytes($cachePaths[0])
361 [IO.File]::WriteAllBytes((Join-Path $evidence 'attachment.txt'), $attachment)
362 Add-Log 'capture_attachment_cache' $Fixture @{ bytes = $attachment.Length }
363 }
364 $native = [pscustomobject][ordered]@{
365 fixture = $Fixture
366 notebook_id = $script:openedNotebookId
367 section_id = $script:sectionId
368 hierarchy_file = 'hierarchy.xml'
369 pages = $pages
370 }
371 Write-Json $native (Join-Path $evidence 'native.json')
372 Add-Log 'capture_native_api' $Fixture @{ page_count = $pages.Count; schema = $schema }
373
374 Sync-CloseGeneratedNotebook $Fixture 'persist'
375 $reopened = Open-GeneratedNotebook $expectedPageCount
376 $reopenedDirectory = Join-Path $evidence 'reopened'
377 if (Test-Path -LiteralPath $reopenedDirectory) { throw "Reopened evidence for '$Fixture' already exists." }
378 New-Item -ItemType Directory -Path $reopenedDirectory | Out-Null
379 $reopenedPages = @(Write-PageEvidence $reopenedDirectory $reopened.hierarchy $reopened.page_ids $reopened.page_contents)
380 Write-Json ([pscustomobject][ordered]@{
381 expected_page_count = $expectedPageCount
382 actual_page_count = $reopenedPages.Count
383 hierarchy_file = 'hierarchy.xml'
384 pages = $reopenedPages
385 }) (Join-Path $reopenedDirectory 'native.json')
386 Add-Log 'reopen_ready' $Fixture @{ expected_page_count = $expectedPageCount; actual_page_count = $reopenedPages.Count }
387 return [pscustomobject][ordered]@{
388 evidence = $evidence.Substring($root.Length).TrimStart('\')
389 reopened_evidence = (Join-Path (Join-Path 'evidence' $Fixture) 'reopened')
390 snapshot = (Join-Path 'snapshots' $Fixture)
391 }
392}
393
394function Invoke-Step {
395 param(
396 [string]$Name,
397 [string]$Description,
398 [scriptblock]$Action
399 )
400
401 try {
402 Add-Log 'begin_fixture' $Name @{ description = $Description }
403 & $Action
404 $capture = Capture-Notebook $Name
405 Add-Fixture $Name 'captured' $Description $capture
406 }
407 catch {
408 Add-Log 'fixture_failed' $Name @{ error = $_.Exception.Message }
409 Add-Fixture $Name 'failed' $Description @{ error = $_.Exception.Message }
410 throw
411 }
412}
413
414function Invoke-PageUpdate {
415 param(
416 [string]$Fixture,
417 [string]$Xml,
418 $Details
419 )
420
421 $input = Join-Path (Join-Path $root 'inputs') "$Fixture.xml"
422 Assert-PathUnderRoot $input | Out-Null
423 [IO.File]::WriteAllText($input, $Xml, [Text.Encoding]::UTF8)
424 $script:app.UpdatePageContent($Xml, [DateTime]::MinValue, $schema, $false)
425 Add-Log 'update_page_content' $Fixture $Details
426}
427
428function Get-OfficeMetadata {
429 $clsid = (Get-Item -LiteralPath 'Registry::HKEY_CLASSES_ROOT\OneNote.Application\CLSID').GetValue('')
430 $server = (Get-Item -LiteralPath "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid\LocalServer32").GetValue('')
431 $match = [regex]::Match($server, '^"(?<path>[^"]+\.exe)"|^(?<path>[^\s]+\.exe)', [Text.RegularExpressions.RegexOptions]::IgnoreCase)
432 if (-not $match.Success) {
433 throw 'OneNote LocalServer32 does not resolve an executable.'
434 }
435 $path = $match.Groups['path'].Value
436 if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
437 throw 'OneNote LocalServer32 executable is unavailable.'
438 }
439 $version = (Get-Item -LiteralPath $path).VersionInfo.FileVersion
440 if ([String]::IsNullOrWhiteSpace($version)) {
441 throw 'OneNote executable has no file version.'
442 }
443 return [pscustomobject][ordered]@{
444 prog_id = 'OneNote.Application'
445 clsid = $clsid
446 local_server = $server
447 executable = $path
448 file_version = $version
449 xml_schema = 'xs2010'
450 xml_schema_value = $schema
451 xml_namespace = $namespace
452 }
453}
454
455function Wait-ForStableFile {
456 param([string]$Path)
457
458 $full = Assert-PathUnderRoot $Path
459 $deadline = (Get-Date).AddSeconds(30)
460 while ((Get-Date) -lt $deadline) {
461 if (Test-Path -LiteralPath $full -PathType Leaf) {
462 $first = Get-Item -LiteralPath $full
463 $firstHash = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant()
464 Start-Sleep -Milliseconds 500
465 $second = Get-Item -LiteralPath $full
466 $secondHash = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant()
467 if ($first.Length -eq $second.Length -and $firstHash -eq $secondHash) {
468 return [pscustomobject][ordered]@{ bytes = [int64]$second.Length; sha256 = $secondHash }
469 }
470 }
471 Start-Sleep -Milliseconds 500
472 }
473 throw 'OneNote did not finish writing the package within 30 seconds.'
474}
475
476$profile = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
477if ((Get-ItemProperty "$profile\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
478 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
479 throw 'Park the personal OneNote profile before running native tests.'
480}
481if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before creating fixtures.' }
482Assert-RunRoot
483
484if ($Mode -eq 'Create') {
485 if (Test-Path -LiteralPath $root) {
486 throw 'Choose a root that does not exist.'
487 }
488 New-Item -ItemType Directory -Path $root | Out-Null
489 foreach ($directory in 'assets', 'evidence', 'inputs', 'snapshots') {
490 New-Item -ItemType Directory -Path (Join-Path $root $directory) | Out-Null
491 }
492 Write-Json ([pscustomobject][ordered]@{
493 format = 'onenote-native-stage1-root-v1'
494 root = $root
495 generated = $true
496 }) $markerPath
497 $script:manifest = [pscustomobject][ordered]@{
498 format = 'onenote-native-stage1-v1'
499 status = 'running'
500 root = $root
501 created_at_utc = [DateTime]::UtcNow.ToString('o')
502 office = Get-OfficeMetadata
503 harness = [pscustomobject][ordered]@{
504 source_path = $PSCommandPath
505 source_sha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
506 }
507 notebook = [pscustomobject][ordered]@{ path = 'notebook'; section = $sectionName }
508 fixtures = @()
509 operation_log = 'operation-log.jsonl'
510 }
511 Save-Manifest
512}
513else {
514 if (-not (Test-Path -LiteralPath $root -PathType Container) -or -not (Test-Path -LiteralPath $markerPath -PathType Leaf)) {
515 throw 'Capture requires a generated corpus root.'
516 }
517 if (((Get-Item -LiteralPath $root -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
518 throw 'Capture root cannot be a reparse point.'
519 }
520 if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf) -or -not (Test-Path -LiteralPath $notebookPath -PathType Container)) {
521 throw 'Capture requires the generated manifest and notebook.'
522 }
523 try {
524 $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json
525 }
526 catch {
527 throw 'Capture requires a readable generated corpus marker.'
528 }
529 if ($marker.format -ne 'onenote-native-stage1-root-v1' -or $marker.generated -ne $true -or
530 -not [String]::Equals([IO.Path]::GetFullPath($marker.root).TrimEnd('\'), $root, [StringComparison]::OrdinalIgnoreCase)) {
531 throw 'Capture root marker does not match this generated corpus.'
532 }
533 if ([String]::IsNullOrWhiteSpace($Step) -or $Step -notmatch '^[0-9]{2}-[a-z0-9-]+$') {
534 throw 'Choose a lowercase numbered fixture name for -Step.'
535 }
536 $script:manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
537}
538
539try {
540 $script:app = New-Object -ComObject OneNote.Application
541 Add-Log 'connect_com' $null @{ prog_id = 'OneNote.Application'; schema = $schema }
542
543 if ($Mode -eq 'Capture') {
544 Open-GeneratedNotebook
545 $capture = Capture-Notebook $Step
546 Add-Fixture $Step 'captured' 'Manual native fixture.' $capture
547 }
548 else {
549 $attachmentPath = Join-Path $root 'assets\fictitious-attachment.txt'
550 $imagePath = Join-Path $root 'assets\fictitious-image.png'
551 [IO.File]::WriteAllText($attachmentPath, 'Fictitious attachment for native corpus.', [Text.Encoding]::UTF8)
552 [IO.File]::WriteAllBytes($imagePath, [Convert]::FromBase64String('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9J1uoAAAAASUVORK5CYII='))
553 Add-Log 'generate_assets' $null @{ attachment = 'assets\fictitious-attachment.txt'; image = 'assets\fictitious-image.png' }
554
555 Invoke-Step '00-empty' 'New notebook and section.' {
556 $notebook = ''
557 $script:app.OpenHierarchy($notebookPath, '', [ref]$notebook, 1)
558 if ([String]::IsNullOrWhiteSpace($notebook)) { throw 'OneNote did not create the generated notebook.' }
559 $script:openedNotebookId = $notebook
560 $section = ''
561 $script:app.OpenHierarchy($sectionName, $notebook, [ref]$section, 3)
562 if ([String]::IsNullOrWhiteSpace($section)) { throw 'OneNote did not create the generated section.' }
563 $script:sectionId = $section
564 Add-Log 'create_notebook_section' '00-empty' @{ notebook_id = $notebook; section_id = $section; path = $notebookPath }
565 }
566
567 Invoke-Step '01-blank-page' 'Blank page with no title.' {
568 $page = ''
569 $script:app.CreateNewPage($script:sectionId, [ref]$page, 2)
570 if ([String]::IsNullOrWhiteSpace($page)) { throw 'OneNote did not create the blank page.' }
571 Add-Log 'create_new_page' '01-blank-page' @{ page_id = $page; style = 'npsBlankPageNoTitle'; value = 2 }
572 }
573
574 Invoke-Step '02-text' 'Plain text outline.' {
575 $pageId = Get-SolePageId
576 $body = '<one:Outline><one:OEChildren><one:OE><one:T><![CDATA[Fictitious plain text.]]></one:T></one:OE></one:OEChildren></one:Outline>'
577 $xml = "<one:Page xmlns:one=`"$namespace`" ID=`"$pageId`">$body</one:Page>"
578 Invoke-PageUpdate '02-text' $xml @{ page_id = $pageId; kind = 'plain_text_outline'; input = 'inputs\\02-text.xml' }
579 }
580
581 Invoke-Step '03-format-unicode' 'Formatted Unicode text replacing the fetched outline.' {
582 $pageId = Get-SolePageId
583 $before = Get-PageXml $pageId
584 [IO.File]::WriteAllText((Join-Path $root 'evidence\03-format-unicode-before.xml'), $before, [Text.Encoding]::UTF8)
585 [xml]$source = $before
586 $manager = New-Object Xml.XmlNamespaceManager($source.NameTable)
587 $manager.AddNamespace('one', $namespace)
588 $outline = $source.SelectSingleNode('//one:Outline[@objectID]', $manager)
589 if ($null -eq $outline) { throw 'OneNote did not assign an objectID to the text outline.' }
590 $text = $outline.SelectSingleNode('.//one:T', $manager)
591 if ($null -eq $text) { throw 'The fetched text outline has no text node.' }
592 foreach ($child in @($text.ChildNodes)) { [void]$text.RemoveChild($child) }
593 $unicode = 'Fictitious: caf' + [char]0x00E9 + ', ' + [char]0x6771 + [char]0x4EAC + ', ' + [char]0x0645 + [char]0x0631 + [char]0x062D + [char]0x0628 + [char]0x0627
594 $html = '<span style="font-family:Calibri;font-size:11.0pt;font-weight:bold;color:#1F4E79">' + $unicode + '</span>'
595 [void]$text.AppendChild($source.CreateCDataSection($html))
596 $change = New-Object Xml.XmlDocument
597 $page = $change.CreateElement('one', 'Page', $namespace)
598 $page.SetAttribute('ID', $pageId)
599 [void]$page.AppendChild($change.ImportNode($outline, $true))
600 [void]$change.AppendChild($page)
601 Invoke-PageUpdate '03-format-unicode' $change.OuterXml @{ page_id = $pageId; object_id = $outline.GetAttribute('objectID'); kind = 'full_outline_replace'; input = 'inputs\\03-format-unicode.xml' }
602 }
603
604 Invoke-Step '04-positioned-outline' 'Positioned text outline.' {
605 $pageId = Get-SolePageId
606 $body = '<one:Outline><one:Position x="144" y="96"/><one:Size width="260" height="72"/><one:OEChildren><one:OE><one:T><![CDATA[Fictitious positioned outline.]]></one:T></one:OE></one:OEChildren></one:Outline>'
607 $xml = "<one:Page xmlns:one=`"$namespace`" ID=`"$pageId`">$body</one:Page>"
608 Invoke-PageUpdate '04-positioned-outline' $xml @{ page_id = $pageId; x = 144; y = 96; input = 'inputs\\04-positioned-outline.xml' }
609 }
610
611 Invoke-Step '05-image' 'Generated PNG in a positioned outline.' {
612 $pageId = Get-SolePageId
613 $data = [Convert]::ToBase64String([IO.File]::ReadAllBytes($imagePath))
614 $body = '<one:Outline><one:Position x="144" y="192"/><one:Size width="96" height="96"/><one:OEChildren><one:OE><one:Image format="png"><one:Data>' + $data + '</one:Data></one:Image></one:OE></one:OEChildren></one:Outline>'
615 $xml = "<one:Page xmlns:one=`"$namespace`" ID=`"$pageId`">$body</one:Page>"
616 Invoke-PageUpdate '05-image' $xml @{ page_id = $pageId; asset = 'assets\fictitious-image.png'; x = 144; y = 192; input = 'inputs\\05-image.xml' }
617 }
618
619 Invoke-Step '06-attachment' 'Generated attachment in a positioned outline.' {
620 $pageId = Get-SolePageId
621 $body = '<one:Outline><one:Position x="264" y="192"/><one:OEChildren><one:OE><one:InsertedFile pathSource="' + [Security.SecurityElement]::Escape($attachmentPath) + '" preferredName="fictitious-attachment.txt"/></one:OE></one:OEChildren></one:Outline>'
622 $xml = "<one:Page xmlns:one=`"$namespace`" ID=`"$pageId`">$body</one:Page>"
623 Invoke-PageUpdate '06-attachment' $xml @{ page_id = $pageId; asset = 'assets\fictitious-attachment.txt'; x = 264; y = 192; input = 'inputs\\06-attachment.xml' }
624 }
625
626 Invoke-Step '07-table' 'Two-cell native table.' {
627 $pageId = Get-SolePageId
628 $body = '<one:Outline><one:Position x="144" y="312"/><one:OEChildren><one:OE><one:Table bordersVisible="true"><one:Columns><one:Column index="0" width="37.11"/><one:Column index="1" width="37.11"/></one:Columns><one:Row><one:Cell><one:OEChildren><one:OE><one:T><![CDATA[Left cell]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell><one:OEChildren><one:OE><one:T><![CDATA[Right cell]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE></one:OEChildren></one:Outline>'
629 $xml = "<one:Page xmlns:one=`"$namespace`" ID=`"$pageId`">$body</one:Page>"
630 Invoke-PageUpdate '07-table' $xml @{ page_id = $pageId; x = 144; y = 312; input = 'inputs\\07-table.xml'; kind = 'two_cell_table' }
631 }
632
633 try {
634 $packagePath = Join-Path $root 'notebook.onepkg'
635 $script:app.Publish($script:openedNotebookId, $packagePath, 1, '')
636 $package = Wait-ForStableFile $packagePath
637 $capture = Capture-Notebook '08-onepkg'
638 Add-Fixture '08-onepkg' 'captured' 'Published OneNote package.' @{ package = 'notebook.onepkg'; bytes = $package.bytes; sha256 = $package.sha256; capture = $capture }
639 Add-Log 'publish_package' '08-onepkg' @{ package = 'notebook.onepkg'; bytes = $package.bytes; sha256 = $package.sha256 }
640 }
641 catch {
642 Add-Log 'fixture_failed' '08-onepkg' @{ error = $_.Exception.Message }
643 Add-Fixture '08-onepkg' 'failed' 'Published OneNote package.' @{ error = $_.Exception.Message }
644 }
645 }
646
647 $script:manifest.status = if (@($script:manifest.fixtures | Where-Object { $_.status -eq 'failed' }).Count -gt 0) { 'completed-with-failures' } else { 'completed' }
648 $script:manifest | Add-Member -NotePropertyName completed_at_utc -NotePropertyValue ([DateTime]::UtcNow.ToString('o')) -Force
649 Save-Manifest
650}
651catch {
652 if ($null -ne $script:manifest) {
653 $script:manifest.status = 'failed'
654 $script:manifest | Add-Member -NotePropertyName failure -NotePropertyValue $_.Exception.Message -Force
655 Save-Manifest
656 }
657 throw
658}
659finally {
660 if ($null -ne $script:app -and -not [String]::IsNullOrWhiteSpace($script:openedNotebookId)) {
661 try {
662 $script:app.CloseNotebook($script:openedNotebookId, $false)
663 }
664 catch {
665 }
666 }
667}