slipcase-open 0.2.1

Open the content file of a Slipcase container in its own application, and write edits back into the container
Documentation
# Run the gate on Windows, and ask the platform the one thing no test can.
#
# `linux.yml` is the older half of this pair and its header explains the shape.
# What is different here is worth stating rather than inferring, because a green
# tick invites more faith than it has earned.
#
# **The suite runs three times rather than once.** `CLAUDE.md` says why and a
# runner is the case it was written about: the tests watch real directories
# through `ReadDirectoryChangesW`, and event timing moves with load. Three
# rather than five only because the runner is slower than a desk.
#
# **A run that wedges is killed and retried, and this is not tolerance of a
# real failure.** `flow::tests::a_save_reaches_the_container_without_anybody_
# closing_the_session` has hung on this runner: the save's event never settles
# the watcher's wait and the test blocks past its own deadline, a rare timing
# race in the `notify` watcher rather than a loop in this crate. Measured
# 2026-09-07: two CI runs wedged, then fifty runs of the exact command did not,
# so instrumentation moves the timing and hides it — a Heisenbug.
# `docs/windows-save-test-hang.md` is the write-up and the way to chase it on a
# Windows machine. Until then the step needs three clean passes, kills a run
# that wedges past three minutes, and tolerates a bounded number of those
# before it calls the wedge real and fails. A run that *fails* is never
# retried; only a wedge is.
#
# **The verdict is taken from cargo and never read out of its output.** That
# distinction has cost this repository twice, and a loop is exactly where it
# would cost it a third time: the pass count comes from each process's exit
# code below, never from a line in its log.
#
# **Nothing here opens a document.** `platform::testing::Recording` is what the
# suite launches through, and on this platform there is nothing to launch
# through yet: `ShellExecuteEx` with `IAttachmentExecute` is still to be written,
# so concept 5 step 7 and the whole of Mark of the Web are unexercised here.
#
# **Nothing here shows a notification.** There is no toast yet, so the terminal
# channel is what runs, and concept 9's buttons are checked by hand.
#
# **Nothing here reads a policy file or a registry key.** `for_this_platform`
# names no source on Windows until the registry arrives, so what the suite
# resolves is the built-in set. The two integration tests that name Linux's
# files are `cfg(unix)` and do not run here, which is why this workflow's count
# is lower than the Linux one's rather than because something was skipped.
#
# **Nothing here packages anything.** MSIX and its signing are still to come.
#
# What only a machine can answer is the last step. The front door's name carries
# the SID of the account the process runs as, read from its own token, and the
# state directory has to be the one the environment names rather than the one
# this account happens to have. Both were written on one desk against one
# profile; a runner is a different account on a different machine, which is the
# entire point of asking here.
#
# Author: David M. Anderson
# Built with AI assistance (Claude, Anthropic)

name: Windows

on:
  push:
  pull_request:
  # The release event is what attaches the package. A person's token creates
  # the release, so the event fires.
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to package for (e.g. v0.1.9)'
        required: true
        type: string

env:
  CARGO_TERM_COLOR: always

permissions:
  contents: write

jobs:
  windows:
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}

      - name: What is installed
        run: |
          rustc -V
          cargo -V

      - name: Formatting
        run: cargo fmt --check

      - name: Clippy
        run: cargo clippy --all-targets -- -D warnings

      - name: Build
        run: cargo build --all-targets

      # Not `check.sh`: that would run fmt and clippy again inside a step that
      # says it is testing, which is the same reason `linux.yml` spells it out.
      #
      # Three clean passes, taken from each run's exit code. A run that wedges
      # past the timeout is killed and does not count, up to a bounded number;
      # a run that fails is fatal at once and never retried. The header says
      # what the wedge is and where the write-up lives.
      - name: Test, three times
        run: |
          $needed = 3       # clean passes required, the CLAUDE.md count
          $maxWedge = 3     # wedges tolerated before a wedge is called real
          $timeoutMs = 180000
          $passes = 0
          $wedges = 0
          while ($passes -lt $needed) {
            Write-Host "run (passes $passes/$needed, wedges $wedges/$maxWedge)"
            $out = Join-Path $env:RUNNER_TEMP "test-$passes-$wedges.log"
            $p = Start-Process -FilePath cargo -ArgumentList @('test','--quiet') `
              -RedirectStandardOutput $out -RedirectStandardError "$out.err" `
              -NoNewWindow -PassThru
            if (-not $p.WaitForExit($timeoutMs)) {
              try { $p.Kill($true) } catch { $p.Kill() }
              $wedges++
              $secs = [int]($timeoutMs / 1000)
              Write-Host "::warning::a test run wedged past ${secs}s and was killed (wedge $wedges/$maxWedge). The known ReadDirectoryChangesW watcher hang; see docs/windows-save-test-hang.md."
              Get-Content "$out.err" -Tail 20 -ErrorAction SilentlyContinue
              if ($wedges -ge $maxWedge) {
                throw "tests wedged $wedges times running to $needed passes; that is not a flake"
              }
              continue
            }
            Get-Content $out -ErrorAction SilentlyContinue
            if ($p.ExitCode -ne 0) {
              Get-Content "$out.err" -Tail 40 -ErrorAction SilentlyContinue
              throw "a test run failed (exit $($p.ExitCode))"
            }
            $passes++
          }
          Write-Host "$needed clean passes (after $wedges wedge(s))"

      # The two things that come out of the environment rather than the source,
      # asked on a machine that is not the one they were written on.
      #
      # The door has to be this account's pipe. Its name is built from the SID
      # in the process token, so a runner proves the lookup works against an
      # account nobody here has seen; and the pipe namespace belongs to the
      # machine, so the SID is what keeps two sessions off one door.
      #
      # The sessions have to be where `%LOCALAPPDATA%` says. That is not
      # hypothetical: the integration suite redirected only `HOME` and the three
      # `XDG_` variables, Windows reads none of them, and the tests were running
      # against the real state directory until the pipe landed and it was found.
      - name: The front door is this account's, and the state is where it is told
        run: |
          $world = Join-Path $env:RUNNER_TEMP 'world'
          New-Item -ItemType Directory -Force -Path $world | Out-Null
          $env:LOCALAPPDATA = $world

          $said = (cargo run --quiet -- policy) -join "`n"
          if ($LASTEXITCODE -ne 0) { throw 'the policy verb refused' }
          Write-Host $said

          # `Contains` rather than `-match`, because the prefix is backslashes
          # and every one of them would have to be escaped for a regex.
          if (-not $said.Contains('\\.\pipe\slipcase-open.S-1-')) {
            throw "the front door is not this account's pipe"
          }
          if (-not $said.Contains($world)) {
            throw 'the state directory was not the one the environment named'
          }

      # slipcase-desktop 0.1.1 was packaged, certified, submitted and failed
      # Store policy 10.2.4.1 for importing VCRUNTIME140.dll from the Visual
      # C++ Redistributable, which every build machine has and a tester's clean
      # one does not. `.cargo/config.toml` is the fix and this is the guard on
      # it, against the release binary because that is what ships. It runs
      # before the package rather than inside it, so a refusal names the import
      # rather than arriving as a failed packaging step.
      - name: Every DLL the shipped binary imports comes with Windows
        run: |
          cargo build --release
          powershell -ExecutionPolicy Bypass -File packaging/windows/check-imports.ps1

      # The Store takes the package unsigned, and the runner assembles the one that
      # is uploaded. The steps are excelano/.github's, because they were the same in
      # every repository that has this target; the publisher is the organisation
      # secret, being the one value in the identity that is an account's rather than
      # this product's.
      - name: The Store package
        uses: excelano/.github/.github/actions/store-package@main
        with:
          publisher: ${{ secrets.STORE_PUBLISHER }}
          tag: ${{ github.event.release.tag_name || inputs.tag }}
          token: ${{ secrets.GITHUB_TOKEN }}