ci: execute complete isolation owners on native Windows

Problem: green PR checks could omit complete isolation/worktree owner tests on
Windows and under Linux race; pure isolation changes could skip native legs.

Root cause: the smoke and race package allowlists lacked both owners, while
the platform classifier treated untagged isolation Go as neutral.

Fix: classify isolation paths as platform-relevant; execute complete Windows
owners with an uploaded JSON inventory, require both package terminals and
portable boundary tests, and include both packages in PR race. Replace the
redundant rollback subset. Preserve existing permissions, deadlines and cache
rules; do not alter product code.

Verification: compiling old-workflow/classifier regressions, exact inventory
positive/negative controls, final classifier/testenv suites, complete 238-package
root inventory, three-owner race, required builtin/boot, vet, lint and check.
Local execution is macOS; native hosted outcomes remain a separate gate.

Refs #14025
Refs #14133
This commit is contained in:
SivanCola committed 2026-10-25 14:02:42 +08:00
1 parent 8c669ebe61
commit 3fa1ea460c
5 files changed
+179 -12

No files matched your search

+32 -6
View File
@@ -198,7 +198,8 @@ jobs:
# Pull requests run a Windows smoke suite: the packages that actually
# carry *_windows.go code, the startup/checkpoint packages with portable
# filesystem contracts, plus cmd/. The full ./... sweep stays on pushes
# filesystem contracts, plus cmd/. The isolation/worktree owners have a
# complete JSON inventory below. The full ./... sweep stays on pushes
# to main-v2; the Unix legs always run the full suite. Keep the job-level
# budget above normal 7-9 minute runner variance; each package test binary
# remains independently bounded by Go's per-package timeout below.
@@ -222,11 +223,36 @@ jobs:
WINDOWS_SANDBOX_WAIT_MS: "20000"
run: go test -count=1 -p 4 -timeout=18m ./internal/runtime/agent/... ./internal/platform/appidentity/... ./internal/assembly/boot/... ./internal/state/checkpoint/... ./internal/frontend/cli/... ./internal/session/control/... ./internal/base/filelock/... ./internal/base/fileutil/... ./internal/ext/hook/... ./internal/state/instruction/... ./internal/ext/mcplaunch/... ./internal/platform/notify/... ./internal/base/proc/... ./internal/platform/remote/... ./internal/platform/repair/... ./internal/safety/sandbox/... ./internal/base/sysproxy/... ./internal/state/workspacelease/... ./cmd/...
# Worktree rollback is absent from the broad Windows PR smoke list.
- name: test (Windows worktree rollback)
- name: test (Windows isolation owners)
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
timeout-minutes: 3
run: go test -count=1 -timeout=2m -run '^TestDiscardUnpublished.*OnWindows$|^TestCreateFromDetachedHEADAndOccupiedBranch$' ./internal/platform/worktree
timeout-minutes: 25
shell: bash
run: |
set -o pipefail
go test -count=1 -p 4 -timeout=18m -json ./internal/runtime/isolation/... ./internal/platform/worktree/... | tee "$RUNNER_TEMP/isolation-owners.jsonl" |
jq -rj --unbuffered 'select(.Action == "output" and .Test == null) | .Output'
jq -se '
[.[] | select(.Test == null and .Action == "pass") | .Package] as $passed |
[.[] | select(.Test != null and .Action == "pass") | .Test] as $tests |
(["reasonix/internal/runtime/isolation", "reasonix/internal/platform/worktree"] - $passed | length) == 0 and
(["TestResultOperationUsesCrossProcessOwnership",
"TestActualResultOperationHoldsOwnershipThroughWriteAndCleanup/restore",
"TestActualResultOperationHoldsOwnershipThroughWriteAndCleanup/remove",
"TestApplyPathsWithUntrustedModesPreservesTheIndex/content",
"TestApplyPathsWithUntrustedModesPreservesTheIndex/source_index_mode",
"TestApplyPathsWithUntrustedModesPreservesTheIndex/candidate_mode",
"TestApplyPathsWithUntrustedModesPreservesTheIndex/candidate_type",
"TestApplyPathsEmulatedLinksUseSnapshotFilters"] - $tests | length) == 0
' "$RUNNER_TEMP/isolation-owners.jsonl"
- name: upload Windows isolation test inventory
if: always() && env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: isolation-owners-windows-${{ github.run_attempt }}
path: ${{ runner.temp }}/isolation-owners.jsonl
if-no-files-found: ignore
retention-days: 7
# The general Windows PR smoke list intentionally omits internal/contract/tool.
# Keep the session-temp portability contract covered without widening the
@@ -315,7 +341,7 @@ jobs:
if: github.event_name == 'pull_request'
env:
REASONIX_RELEASE_CACHE_GUARD: "1"
run: go test -race ./internal/runtime/agent/... ./internal/ext/plugin/... ./internal/tools/jobs/... ./internal/base/proc/... ./internal/safety/sandbox/... ./internal/base/filelock/... ./internal/contract/eventwire/... ./internal/platform/remote/... ./internal/ext/extension/... ./internal/assembly/boot/... ./internal/session/control/... ./internal/contract/tool/...
run: go test -race ./internal/runtime/agent/... ./internal/runtime/isolation/... ./internal/platform/worktree/... ./internal/ext/plugin/... ./internal/tools/jobs/... ./internal/base/proc/... ./internal/safety/sandbox/... ./internal/base/filelock/... ./internal/contract/eventwire/... ./internal/platform/remote/... ./internal/ext/extension/... ./internal/assembly/boot/... ./internal/session/control/... ./internal/contract/tool/...
- name: test -race (full)
if: github.event_name != 'pull_request'
+81
View File
@@ -0,0 +1,81 @@
---
owner: @SivanCola
backup: @esengine
status: active
reviewed: 2026-10-25
---
# Isolation CI coverage / 隔离机制 CI 覆盖
## Gap and repair
An all-green CI summary did not mean the isolation owners ran on native
Windows. PRs used a smoke package list; the extra worktree step selected only
rollback tests. The PR race list also omitted both full owning packages.
Pure internal/runtime/isolation changes could additionally be classified as
platform-neutral because that package has no OS-constrained file. Its Git,
filesystem and process contracts are nevertheless platform-specific.
The path gate now names that owner explicitly. Windows PRs execute complete
isolation and worktree packages with JSON terminal inventory; Linux PR race
checks include both owners. The full worktree run replaces the redundant
rollback-only step.
## Actual matrix
| Surface | PR coverage |
| --- | --- |
| Platform decision | Isolation production/test paths request native legs; existing neutral/unknown-path policies stay intact |
| Linux and macOS | Existing full root suites, when the native leg is required |
| Windows isolation/worktree | Native uncached complete package runs, with both package-pass terminals required |
| Windows file locks | Existing full filelock package in the broad smoke command |
| Linux race | Both complete owning packages join the existing concurrency suite |
| Windows inventory | Uploaded JSON includes individual outcomes and conditional skips |
| Full push suite | Existing full root Windows/race checks remain unchanged |
Windows owner tests use the established 18-minute package and 25-minute step
bounds, with -count=1 and package concurrency four. Product deadlines, the
broad smoke timeout, workflow permissions and runner labels are unchanged.
Record observed durations before making future capacity adjustments.
The inventory requires terminal passes for both packages and eight portable
ownership/checkout cases. Package passes with those cases skipped cannot pass
the gate. An earlier failing step leaves the job failed; a missing upload never
establishes a passing owner result.
## Regression and security scope
Real path-gate tests fail on the preceding classifier for pure isolation paths.
The existing YAML test owner now also checks that unfiltered PR commands select
both complete packages on native Windows and with race enabled. A focused
rollback command cannot satisfy that coverage contract.
No application Go, data format, tool permission or default is changed. No new
secret, credential, privileged trigger or write scope is introduced. Artifact
upload reuses the existing pinned action and seven-day retention; its input is
only the disposable test inventory.
## Evidence limits
Workflow configuration and local tests do not prove a native run happened.
Read the actual checkout, job outcome and JSON before claiming platform evidence.
POSIX-only skips do not count as Windows behavior coverage; Windows amd64
functional tests do not prove Windows ARM64 or Windows race support.
Individual package passes do not establish the full concurrency/isolation plan.
Crash recovery, safe retention, snapshot-bound verification, compatibility,
performance and complete combined-tree acceptance remain separate requirements.
## 中文说明
此前 Windows PR 白名单没有执行完整的隔离与 worktree 所有者包,额外步骤只测了
回滚子集;Linux PR race 清单也缺少这两个包。纯隔离运行时代码还可能被误判为
平台无关,导致原生检查完全跳过。
现在平台准入明确包含隔离所有者,Windows 执行两个完整包并要求各自成功结束;
原始 JSON 上传用于核对实际执行、条件跳过和故障。完整 worktree 测试包含原有
回滚用例,不重复运行子集。Linux race 同时加入两个完整所有者包。
沿用已有超时、并发数、权限、固定版本 action 和结果缓存规则,不改变产品行为。
本地命令、工作流配置和宿主 CI 是不同证据;实际原生结果必须按提交及平台核实。
+63 -5
View File
@@ -86,11 +86,8 @@ func goTestArgs(script string) [][]string {
return out
}
// A root-module package list run on Windows with go test's result cache on
// pays, before and after each package, one filepath.EvalSymlinks per path its
// binary recorded outside the module. internal/assembly/boot records ~1.9M, so
// the step spends minutes printing nothing and runs into its job timeout.
func TestWindowsCILegsBypassTheTestResultCache(t *testing.T) {
func readCIWorkflow(t *testing.T) ciWorkflow {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "..", ".github", "workflows", "ci.yml"))
if err != nil {
t.Fatal(err)
@@ -99,6 +96,15 @@ func TestWindowsCILegsBypassTheTestResultCache(t *testing.T) {
if err := yaml.Unmarshal(raw, &wf); err != nil {
t.Fatal(err)
}
return wf
}
// A root-module package list run on Windows with go test's result cache on
// pays, before and after each package, one filepath.EvalSymlinks per path its
// binary recorded outside the module. internal/assembly/boot records ~1.9M, so
// the step spends minutes printing nothing and runs into its job timeout.
func TestWindowsCILegsBypassTheTestResultCache(t *testing.T) {
wf := readCIWorkflow(t)
checked := 0
for jobName, job := range wf.Jobs {
if !job.runsOnWindows() {
@@ -120,3 +126,55 @@ func TestWindowsCILegsBypassTheTestResultCache(t *testing.T) {
t.Fatal("ci.yml has no root-module go test step that runs on Windows; this guard no longer reads the workflow's shape")
}
}
func prOwnerTestArgs(wf ciWorkflow, windows bool) [][]string {
job := wf.Jobs["race"]
if windows {
job = wf.Jobs["test"]
if !job.runsOnWindows() {
return nil
}
}
var commands [][]string
for _, step := range job.Steps {
if !job.rootModule(step) || strings.Contains(step.If, "github.event_name != 'pull_request'") ||
(windows && strings.Contains(step.If, "runner.os != 'Windows'")) {
continue
}
for _, args := range goTestArgs(step.Run) {
if (!windows && !slices.Contains(args, "-race")) || slices.ContainsFunc(args, func(arg string) bool {
return arg == "-run" || strings.HasPrefix(arg, "-run=") || arg == "-list" || strings.HasPrefix(arg, "-list=")
}) {
continue
}
commands = append(commands, args)
}
}
return commands
}
func coversPackage(args []string, target string) bool {
return slices.ContainsFunc(args, func(pattern string) bool {
if pattern == target || pattern == "./..." {
return true
}
prefix, recursive := strings.CutSuffix(pattern, "/...")
return recursive && (target == prefix || strings.HasPrefix(target, prefix+"/"))
})
}
func TestPRCIExecutesIsolationOwnersOnWindowsAndRace(t *testing.T) {
wf := readCIWorkflow(t)
for _, windows := range []bool{true, false} {
t.Run(map[bool]string{true: "native_windows", false: "race"}[windows], func(t *testing.T) {
commands := prOwnerTestArgs(wf, windows)
for _, owner := range []string{"./internal/runtime/isolation", "./internal/platform/worktree"} {
t.Run(owner, func(t *testing.T) {
if !slices.ContainsFunc(commands, func(args []string) bool { return coversPackage(args, owner) }) {
t.Fatalf("PR CI does not execute the full owning package %s in this matrix leg", owner)
}
})
}
})
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ set -euo pipefail
neutral_dir='^(docs/|release-notes/|workers/|benchmarks/|npm/|examples/|desktop/frontend-next/)'
# Kernel packages whose behaviour is OS-specific (files, processes, shells,
# sandboxes, sockets, paths) even where no file carries a build tag.
platform_dir='^(internal/(safety|platform|base|state|ext|tools|session|assembly)/|cmd/|sdk/|tools/|scripts/|desktop/(electron|cmd|packaging|computer-helper|internal)/|\.github/)'
platform_dir='^(internal/(safety|platform|base|state|ext|tools|session|assembly)/|internal/runtime/isolation/|cmd/|sdk/|tools/|scripts/|desktop/(electron|cmd|packaging|computer-helper|internal)/|\.github/)'
platform_file='(^|/)(go\.(mod|sum)|Makefile)$|\.(nsh|nsi|ps1|sh|bat|cmd)$|_(windows|darwin|linux|unix|bsd)(_test)?\.go$'
seen=0
+2
View File
@@ -19,6 +19,8 @@ test("neutral paths do not need the macOS and Windows legs", () => {
test("platform paths and unknown paths need them", () => {
for (const f of [
"internal/safety/sandbox/a.go",
"internal/runtime/isolation/store.go",
"internal/runtime/isolation/durable_test.go",
"internal/platform/update/a.go",
"cmd/reasonix/main.go",
"desktop/electron/main.ts",