Repository navigation
feat(harness): add the application registry, the main-actor driver, and tessera launch - #65
Merged
Merged
Conversation
…ort slice, and record the catalog capability audit Record the owner's 2026-10-23 decisions in plan 036: Flow and ViewThatFits get their own slice before styling, the terminal colour-report probe becomes Slice 1 and runs in parallel, catalog requirements gain Focus, Pointer, Scroll, and Clock subsections that the capability slices work through, and the coverage walk reports levels, key-and-count routes, and stop reasons. Renumber the slices, add unattended-execution rules, update the developer tooling page, and add research record 043 with the audit's placements and drafted gap requirements for owner review.
Add the package-level seams Slice 2 of plan 036 lists, adapted from the seams spike patch to today's sources, with no new public API: - InMemoryTerminalDevice: package init(...keepsInputOpen:), deliverResize(to:), and closeInput(). An open device registers its byte and size-change streams when terminalDevice is read, so a resize delivered afterwards is never lost and late readers see no replayed history. - InMemoryTerminalSession: package init(device:) to wrap an open device. - TerminalSession: lastDrawn (buffer, cursor, sequence; set after a successful flush), size, and outputTap (teed before the draw's io.write); withApplicationTerminal(configuration:io:...) forwards probeImageID and probeSleep. - ViewGraph: a package init taking the clock the passes read. The seam tests live in TerminalSessionTests and ViewGraphClockTests.
Make TerminalSession.draw a nonisolated(nonsending) entry point, per the runtime isolation design in the Phase 5 spec proposal: the frame body runs in the caller's isolation, so a @mainactor host renders a non-Sendable graph where it lives, and the session serializes draws and commits the finished buffer on itself. sending is dropped from the body and the result. The draw splits into beginFrame (size and rendering policy, fixed once), the body in the caller's isolation, and commitFrame (encode, tee outputTap, write, flush, then set lastDrawn after a successful flush). The seam semantics are unchanged and nothing new is public. A @mainactor test host draws through an InMemoryTerminalDevice session; the same code did not compile against the sending signature.
# Conflicts: # Package.resolved
The draw body now runs in the caller's isolation, so the scope that owns the session can close between beginFrame and the commit. commitFrame checks that the session is open before it encodes, taps, or writes, so a draw caught in that window throws TerminalSessionError.closed instead of painting a frame onto the restored terminal. Document that deliverResize(to:) also emits where the public resize(to:) only changes the reported size, amend the Phase 5 spec's draw transaction and plan 036's Slice 2 text to match, and narrow the Delivers line to the main-actor host the test uses.
TesseraRuntime holds the app contract: Application refines only AnyObject, with body, apply(_:), and a defaulted isFinished, and its documentation carries the four adoption shapes. Tessera re-exports it, and TesseraRuntimeTests compiles each shape against import Tessera. TesseraHarness is a library product beside TesseraTestSupport. It carries the HeadlessRunner spike's driver, scenario parser, runner, report, and registry with its scenario corpus and tests, every public declaration now package. The spike stays under spikes/ as frozen evidence. Both targets get boundary allowlist rows and the shared swiftSettings, docs:targets and docs:merge know TesseraRuntime, and .gitattributes pins scenario files to LF.
Each view component ships its own demo instead of one monolithic app. Delete Examples/Sources/TesseraShowcase, its tests and Xcode schemes, the core:showcase task, and the manifest entries only the Showcase used, and point README.md, CONTRIBUTING.md, and docs/project-status.md at core:example.
…ement Check the Slice 3 box with its evidence, move Next: to Slice 4, record the copy-not-move, access, dependency, shim, and .gitattributes decisions the owner may want to weigh, add the discoveries, and update the changelog.
# Conflicts: # docs/plans/phase-4-view-layer-and-developer-tooling.md
…the unused CLI Qualify the Application docs and the spec: leaving @mainactor off an extension conformance is a compile error from another module and in plain Swift 6 mode, but a module with default main-actor isolation or InferIsolatedConformances infers the isolated conformance for its own types. Add Examples/Sources/ApplicationConsumer, a one-file target that imports only Tessera and declares an Application, so the Examples build fails if Tessera stops re-exporting TesseraRuntime. Remove the harness's CLI, which had no caller and no test; Slice 4 writes HarnessCLI fresh and the spike keeps the original. Record in plan Slice 4 that its driver snapshot test restores the graph-to-session-to-bytes coverage the Showcase test gave, and note the three findings under Discoveries.
A scratch two-module SwiftPM package on Swift 6.3.3 and 6.4 settled when a bare extension conformance on a main-actor type compiles: it is an error in plain Swift 6 mode, builds with default main-actor isolation in the extension's module, and builds under InferIsolatedConformances (on 6.3.3 only for a type declared @mainactor, not one isolated through its module's default isolation). Rewrite the Application docs, the spec paragraph, and the plan's Discoveries and Slice 4 Verify to say so, run Slice 4's second fixture in plain Swift 6 mode, and describe ShotArtifact without the removed CLI.
…nd tessera launch ApplicationRegistry and Viewport are the first public names in TesseraHarness. The registry selects an app by name through a main-actor factory opened once per run, and a duplicate name makes every read of it fail. `mobile` is 40 columns at the app's natural height, measured with a package nil-height proposal on ViewGraph and two tall probe heights; an app whose root grows with the proposal fails `size mobile` naming `mobileScreen`. TesseraRuntime gains the package Step.run(_:graph:event:isolation:), and the harness gains the @mainactor Driver over it, shared by a headless host (a session over a headless device into a Ghostty virtual terminal), a live host, and a per-test actor host. HarnessCLI.main(registry:) runs apps, launch NAME, and help. DemoSupport adds the Demo protocol, isQuitKey, and a counter demo; Examples/Sources/TesseraDev builds it as the tessera executable, with mise run dev:apps and dev:launch. Two compile-fail fixtures with positive controls pin the Application isolation rules, run by quality:architecture. ApplicationConsumer is deleted, since DemoSupport imports Tessera from outside the root package. The tessera-devtools skill documents what is usable.
apps --json writes minimum: null (and a failure writes command: null) instead of omitting the key. launch checks that stdin is a terminal before the lifecycle starts, so it exits 3 with a clear message instead of failing inside raw mode. The registry's headless path is tested for every adoption shape, including the main-actor app. CI and ci:build-test now run swift test --package-path Examples. The step's doc and the spec say a synchronous nonisolated caller cannot compile the call, and the spec's runtime loop delivers the starting size as a resize, as the driver does. The docs call the driver and its hosts internal, as the code has them.
…ntime # Conflicts: # CHANGELOG.md # docs/plans/phase-4-view-layer-and-developer-tooling.md
Sort the root and Examples manifests alphabetically, state enduring facts in comments and DocC instead of plan numbers, conform Locator and FrameExpectation to CustomStringConvertible, say what Application's class-only contract means, express the scenario tests as snapshots, and record the owner's approval of the Slice 3 decisions.
… phase-4/04-driver # Conflicts: # CHANGELOG.md # Package.swift # Sources/TesseraHarness/Driver.swift # Tests/TesseraHarnessTests/Fixtures.swift # Tests/TesseraHarnessTests/ScenarioTests.swift # docs/plans/phase-4-view-layer-and-developer-tooling.md
Sort the conformance lists of RegistryError and StillLabel, express the driver, CLI, registry, viewport, and demo tests as inline snapshots and expectNoDifference where they check structured state, and give the DemoSupport tests the snapshot libraries they need.
… and a contributor skill
…arser TesseraHarness alone depends on ArgumentParser, which parses the arguments, writes the help, and completes app names for fish, zsh, and bash. HarnessCLI keeps the 1.0 promises it would not: a usage error exits 2 where ArgumentParser exits 64, the JSON object and the text form are chosen from the raw --json and --pretty so a failed parse is reported in the form asked for, and a usage error lists what is accepted.
With no name, dev:launch prints the command's help and the registered apps and exits 0 instead of mise's missing-argument error, and dev:launch completes the app names from the tessera binary a previous run built, printing nothing rather than building when there is none.
A dev task ran `swift run` for each tessera call, so one that calls it twice printed two SwiftPM build blocks. It now builds once with `swift build --quiet`, which prints nothing when it succeeds and still prints the compiler's errors when it fails, and runs the built binary for every call.
…pletion as the scripts ask help takes one command: `help launch extra` is a usage error that lists the commands again, and `help help` is pinned. The completion tests send the request the shipped scripts send (positional@0 and the option's flag) instead of the legacy argument name, for every command that takes an app name.
… terminal A task run without its argument, or with an empty one, printed help and exited 0 even in a script, where an unset variable then passed for success. It exits 0 at a terminal and 2 in a pipe or a script, as tessera does.
robfeldmann
marked this pull request as ready for review
October 24, 2026 20:37
# Conflicts: # CHANGELOG.md # CONTRIBUTING.md # Examples/Package.swift # Package.swift # README.md # Sources/TesseraHarness/Driver.swift # Sources/TesseraHarness/Scenarios/Runner.swift # Sources/TesseraHarness/Scenarios/Scenario.swift # Sources/TesseraRuntime/Application.swift # Tests/TesseraHarnessTests/ScenarioTests.swift # docs/architecture/developer-tooling.md # docs/plans/phase-4-view-layer-and-developer-tooling.md # scripts/check-package-boundaries.py # scripts/test_check_package_boundaries.py
Slice 4 deletes ApplicationConsumer and moves the harness's demos and registry; merging the squashed #64 re-added them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan 036 Slice 4, stacked on #64 (
phase-4/03-harness-runtime).What this adds
ApplicationRegistryandViewport(public) inTesseraHarness, withHarnessCLI.main(registry:)(public,async -> Never):apps,launch NAME,help [COMMAND].packageStep.run(_:graph:event:isolation:)inTesseraRuntime, written as the spec's canonical step, and thepackage@MainActorDriverover it. A headless host (a session over a headless device into one Ghostty virtual terminal), a live host, and a per-test actor host share the step; the first two shareTerminalSession.draw.mobilerule: 40 columns at natural height, from apackagenil-height proposal onViewGraph(measuredRootSize(proposing:)) plus measurements at two tall heights. A height-greedy app failssize mobilenamingmobileScreen.Examples/Sources/DemoSupport(Demo,DemoFixture,isQuitKey,Demos.registry(), the counter demo) andExamples/Sources/TesseraDev, thetesseraexecutable, withmise run dev:appsanddev:launch.ApplicationConsumeris deleted.scripts/check-isolation-fixtures.shfromquality:architecture.tessera-devtoolsskill and itsAGENTS.mdpointer; the Developer tooling page, the layout spec, README, CONTRIBUTING, and CHANGELOG say what is built.Verification
All on the baseline toolchain (Xcode 26.6) unless noted; none of the diff touches a
Package.resolved.mise run quality:format,mise run core:test(750 tests; 700 on the base),swift test --package-path Examples(23; 13 on the base),mise run quality:lint(itsquality:architectureruns the boundary checker's unit tests and both isolation probes),mise run docs:lint: green.--scratch-path, deleted after): root build and all 750 tests, Examples tests (23), and both isolation probes pass.checkedEntries,names,entry(named:)(for a unique name too),tessera apps,tessera launch(exit 3), and a scenario run (a failing report with verbapp). Registration itself never throws.size mobilewith a message namingmobileScreen; a natural-height app runs at 40 columns by its measured rows;size mobileScreenruns the greedy app at 40x16.DriverTestsrenders aViewGraphthroughTerminalSession.drawinto anInMemoryTerminalDeviceand asserts the exact bytes the device received (alternate screen, first frame, the changed cell, teardown), which restores the coverage the retired Showcase'sShowcaseTerminalSessionTestsgave.Step.run; an@Observableapp steps like any other.Browserfails with "main actor-isolated conformance of 'Browser' to 'Application' cannot be used in actor-isolated context" (the script matches the shared prefix);extension Journal: Application {}from a plain Swift 6 module fails with "crosses into main actor-isolated code and can cause data races"; both controls compile. A wrong expectation fails the script.Examples/Tests/DemoSupportTests) run the counter headless atdesktopandmobile.tessera launchlive, under a ptyexpectspawnedstty rows 24 cols 80; exec tessera launch counter(and, separately,mise run dev:launch counter), sent+ + + - q, and logged the output; the screens below are that log replayed (frames 0 to 4 of 6, top rows of each):Decisions and findings for the owner
Recorded in the plan under Decisions and Discoveries; the ones marked "(owner to weigh)" are reversible:
HeadlessTerminalDevice, notInMemoryTerminalDevice:TesseraTerminalTestSupportlinks swift-snapshot-testing, and an executable that links it fails at launch (dyld: Library not loaded: @rpath/Testing.framework/Versions/A/Testing, observed on the firsttessera apps). The proper fix moves the SnapshotTesting-free device into its own target, which changes where a public type lives, so it is yours..resizethrough the same step; the spec's runtime loop does not, but the moved scenario corpus and any size-aware demo need it.app NAMEheader (no global default app exists once the registry is a value).packageAPI with@testable import TesseraHarness; the public scenario runner replaces that route later.HarnessCLIlives inTesseraHarness(the stability list names it there), andTesseraDevis a one-linemain.swift.Review follow-ups
The critic review's six nits are addressed on top:
apps --jsonwritesminimum: null(and a failurecommand: null) instead of omitting the key; the docs say the driver and its hosts are internal, as the code has them; the step's doc and the spec say a synchronous nonisolated caller cannot compile the call; CI (Test Examplesinci.yml, andci:build-test) runsswift test --package-path Examples;launchchecks that stdin is a terminal before the lifecycle starts, so it exits 3 with "standard input is not one" instead of failing inside raw mode; and the registry's headless path is tested for every adoption shape, including the main-actor app. The spec's runtime loop, andApplication.apply's docs, now deliver the starting size as a.resizetoo.CI
Green on
e707aa0(run 40067583854, 14 jobs) before the follow-ups:test (macos-26),test (ubuntu-latest),test (ubuntu-latest / swift-6.4.1),test (windows-latest),test (xcode-27),lint, and the version consumers. The follow-ups' run is the one to read.