Skip to content

refactor(ios): name the runner session xctestrun as a process-identity contract - #2987

Open
thymikee wants to merge 1 commit into
mainfrom
refactor/runner-session-xctestrun-name-contract
Open

thymikee wants to merge 1 commit into
mainfrom
refactor/runner-session-xctestrun-name-contract

Conversation

@thymikee

@thymikee thymikee commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #2963 from the same inspection pass: what does runner process cleanup actually depend on, and is it declared anywhere?

A runner launch is killed by matching xcodebuild's argv with pkill -f, and that argv carries the per-session .xctestrun filename. The filename is a process-identity contract, but it was spelled three times: inline in the session, inline in disposal, and again in the daemon-client timeout sweep. Disposal escaped the device id for regex without flattening it the way the writer's filesystem sanitization does, so a device id needing flattening built a pattern that could not match the file the writer had already written.

runner-artifact-env.ts now owns the stem, suffix field order, sanitization, and the derived pkill -f pattern; the writer, the session, and disposal all go through it. Runtime strings are byte-identical.

The name lives in the writer, not a shared contracts module, because the timeout sweep must not follow a rename: it ships separately and has to keep matching names older writers used, so it keeps a pinned literal. That also keeps the eager-closure budget (#1960) at zero new edges — the first shape of this PR added a contracts module and the gate rejected it on four entries.

Also records three tooling constraints from xcodebuild(1) / simctl help, checked on Xcode 27.1: only TEST_RUNNER_-prefixed names cross into the test runner; simctl launch --stdout/--stderr resolve inside the device's data container; and xcodebuild re-synthesizes attachment lifetimes from its own defaults (plan sets keepNever, built xctestrun carries SystemAttachmentLifetime=deleteOnSuccess).

9 files. Production +96/−13, tests +149/0.

Validation

Head 0683a3d46:

  • pnpm check:affected --run passes: eager-closure budgets 691 green, layering guard OK, check:fallow --base origin/main clean in changed files, related Vitest green. Also passing: typecheck, lint, format:check.
  • apple-runner project 627 tests and unit-core daemon-client + eager-closure 805 tests green.
  • Byte parity: old vs new owned/tokenless/suffix patterns compared for concrete, host-style, and flattening device ids — equal everywhere except the flattening case, where the old pattern failed to match its own file (the defect fixed here).
  • Mutation check: renaming the stem fails 5 tests across the three sites.
  • Pins are literals, not derived. xcodebuild .*AgentDeviceRunner\.env\.session- is asserted to still match both owner-token and pre-owner-token (session-<device>-<port>, shipped through v0.17.0) names.
  • Names, argv, and pkill bytes are unchanged, so no device-facing change and no simulator run is claimed. unit-ci, integration, and Swift runner lanes stay GitHub-authoritative.

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.85 MB 4.85 MB +200 B
Package (unpacked) 4.85 MB 4.85 MB +200 B
Package (download) 1.45 MB 1.45 MB +96 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 22.7 ms 22.9 ms +0.1 ms
CLI --help 64.1 ms 66.1 ms +2.0 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/contracts/src/runner-session-artifact.ts">

<violation number="1" location="packages/contracts/src/runner-session-artifact.ts:57">
P3: The contract's sanitizer is private, yet the writer at `packages/platform-apple/src/runner/runner-artifact-env.ts:48` still spells the same rule itself (`suffix.replaceAll(/[^a-zA-Z0-9._-]/g, '_')`). Two copies of the `[a-zA-Z0-9._-]` character set now govern on-disk session names; they're identical and the writer's pass is idempotent today, but any future edit to one copy silently renames sessions while the matchers keep the old bytes — exactly the drift this PR exists to prevent. Export `sanitizeRunnerSessionNameField` and have the writer consume it, or delete the writer's re-sanitize pass.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic


/** Characters a session name may carry; anything else is flattened, as the filesystem writer does. */
function sanitizeRunnerSessionNameField(value: string): string {
return value.replaceAll(/[^a-zA-Z0-9._-]/g, '_');

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The contract's sanitizer is private, yet the writer at packages/platform-apple/src/runner/runner-artifact-env.ts:48 still spells the same rule itself (suffix.replaceAll(/[^a-zA-Z0-9._-]/g, '_')). Two copies of the [a-zA-Z0-9._-] character set now govern on-disk session names; they're identical and the writer's pass is idempotent today, but any future edit to one copy silently renames sessions while the matchers keep the old bytes — exactly the drift this PR exists to prevent. Export sanitizeRunnerSessionNameField and have the writer consume it, or delete the writer's re-sanitize pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/contracts/src/runner-session-artifact.ts, line 57:

<comment>The contract's sanitizer is private, yet the writer at `packages/platform-apple/src/runner/runner-artifact-env.ts:48` still spells the same rule itself (`suffix.replaceAll(/[^a-zA-Z0-9._-]/g, '_')`). Two copies of the `[a-zA-Z0-9._-]` character set now govern on-disk session names; they're identical and the writer's pass is idempotent today, but any future edit to one copy silently renames sessions while the matchers keep the old bytes — exactly the drift this PR exists to prevent. Export `sanitizeRunnerSessionNameField` and have the writer consume it, or delete the writer's re-sanitize pass.</comment>

<file context>
@@ -0,0 +1,62 @@
+
+/** Characters a session name may carry; anything else is flattened, as the filesystem writer does. */
+function sanitizeRunnerSessionNameField(value: string): string {
+  return value.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
+}
+
</file context>
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 25f05bd. The rename to a process-identity contract looks correct. For realistic device ids, the runner argv and the disposal pkill pattern keep the same bytes. The only behavior change is the matcher for device ids outside the sanitized charset. I found no blocking defects. There are no conflicts.

Before this goes further, one design question. The new contracts subpath is justified, because src/daemon-client cannot import platform-apple. But does it need the whole name builder? A smaller contract could export only the stem and the name-prefix pattern and let the writer own the one sanitizer. Then lease-backed disposal could match the lease's recorded xctestrunPath basename directly, instead of rebuilding <device>-<token>- from parts. That would also close the detached-lease gap below. What stops that smaller shape? It would need the disposal adapter to take the lease (or its xctestrunPath) instead of (deviceId, ownerToken).

A pre-existing gap inside the invariant this PR names: a detached lease rewrites ownerToken to detached-<token>, so the reclaim pkill at runner-lease.ts#L554 builds a prefix that never matches the written xctestrun name. Only the pid-tree kill covers it. This is not a regression from this PR; a follow-up is fine.

Smaller notes, not blocking: the writer still re-sanitizes the suffix with its own regex (runner-artifact-env.ts#L48); the round-trip test comment claims more than it pins, because a stem or prefix rename would pass on both sides (runner-xctestrun.test.ts#L366); and the dead AGENT_DEVICE_RUNNER_PORT entry (runner-process-launch.ts#L88) is outside this change.

Smoke Tests, Repo Guards, Coverage and Integration were still running. They all exercise this diff, so a failure there is likely related until shown otherwise. No live simulator run is needed: argv and pkill bytes are unchanged for real device ids.

…identity contract

A runner launch is killed by matching `xcodebuild`'s argv with `pkill -f`, and
that argv carries the per-session xctestrun's filename. The filename is
therefore a process-identity contract, but it was spelled three times: inline in
the session, inline in disposal, and again in the daemon-client timeout sweep.
Disposal escaped the device id for regex without flattening it the way the
writer's filesystem sanitization does, so a device id needing flattening built a
cleanup pattern that could not match the file the writer had already written.

Name it once. `runner-artifact-env.ts` now owns the stem, the suffix field
order, the sanitization, and the `pkill -f` pattern derived from those same
parts, and the writer, the session, and disposal all go through it. The name
lives in the writer rather than a shared contracts module because the timeout
sweep must not follow a rename: it ships separately, cannot know which version
named a timed-out launch, and has to keep matching the names older writers used.
So the sweep keeps a pinned literal, and its test proves those bytes still
select both the owner-token and the pre-owner-token spelling. Placing the name
in the module the Apple façades already evaluate also holds the eager-closure
budget (#1960) at zero new edges.

Verified byte-for-byte against the previous patterns for concrete, host-style,
and flattened device ids. The only divergence is the flattened case, where the
old pattern failed to match its own file.

Also record three tooling constraints read from `xcodebuild(1)` and `simctl
help`, and checked on Xcode 27.1: only `TEST_RUNNER_`-prefixed names cross into
the test runner; `simctl launch --stdout/--stderr` resolve their paths inside
the device's data container, so `--console-pty` is the console mode this host
path can use; and xcodebuild re-synthesizes the attachment-lifetime keys from
its own defaults, since a plan setting `keepNever` still builds an xctestrun
carrying `SystemAttachmentLifetime=deleteOnSuccess`.
@thymikee
thymikee force-pushed the refactor/runner-session-xctestrun-name-contract branch from 25f05bd to 0683a3d Compare September 25, 2026 20:14

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant