Skip to content

Commit 6761d36

Browse files
autopilot: address PR #19 review feedback
Why: - Resolve actionable GitHub review feedback for PR #19. Changed: - Repaired the implementation verification environment by restoring the lockfile-declared macOS ARM64 Rolldown binding. The exact failed harness command now passes 12 test files and 110 tests; the harness TypeScript build also passes. Product verification had already passed, spec-only passed, and the four-file review patch remains unchanged. Verification: - npm run agent:complete -- --session-dir [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session] - npm run agent:context -- --paths-file [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/actual-paths.txt] --session-dir [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session] - npm run agent:verify -- --profile implementation --paths-file [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session/verify/implementation/paths.txt] --session-dir [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session] - npm run agent:verify -- --profile spec-only --paths-file [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session/verify/spec-only/paths.txt] --session-dir [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session] Affected: - CHANGELOG.md - Casks/simulator-broker.rb - Formula/simbroker.rb - README.md - app/README.md - app/Sources/BrokerDashboardStore.swift - app/Sources/BrokerLocalCommandClient.swift - app/Sources/BrokerOnboardingCommands.swift - app/Sources/BrokerServiceClient.swift - app/Sources/BrokerSetupModels.swift - app/Sources/BrokerSnapshotLoader.swift - app/Sources/RootView.swift - app/Sources/SetupPlanDevicesView.swift - app/Sources/SetupPlanPrerequisitesView.swift - app/Sources/SetupPlanSheet.swift - app/Sources/SharedViews.swift - app/Tests/BrokerDashboardStoreTests.swift - app/Tests/BrokerLocalCommandClientTests.swift - app/Tests/BrokerOnboardingCommandsTests.swift - app/Tests/BrokerRuntimePathsTests.swift - app/Tests/BrokerServiceClientTests.swift - app/Tests/BrokerSnapshotLoaderTests.swift - broker-core/error-contract.mjs - broker-core/index.mjs - broker-core/simctl.mjs - broker-core/test/broker-core.test.mjs - client/bin/simbroker.mjs - client/command-dispatch.mjs - client/service/service-client.mjs - client/setup-preflight.mjs - client/setup-provisioning-worker.mjs - client/setup-provisioning.mjs - client/test/setup-preflight.test.mjs - client/test/simbroker.test.mjs - docs/concepts.md - docs/getting-started.md - docs/status.md - docs/test/front-door.test.mjs - examples/harness-adoption/README.md - examples/harness-adoption/sample-consumer-repo/.simulator-broker/project.json - examples/harness-adoption/sample-consumer-repo/README.md - packages/simbroker/README.md - scripts/install_smoke.sh - spec/README.md - spec/architecture.md - spec/build-and-test.md - spec/global-simulator-broker.md - spec/project-structure.md - spec/tasks/README.md - spec/tasks/guided-simbroker-setup.md Refs: - #19 - #19 (comment) - #19 (comment) Session: - task-session: [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/task-session] - report: [controller artifact: jobs/pr-19/20260823-132258-a616d5e1-1203-4223-93c3-3e5f1bfddac7/report.md]
1 parent 24c5f67 commit 6761d36

4 files changed

Lines changed: 123 additions & 27 deletions

File tree

broker-core/index.mjs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,14 +1488,33 @@ function sameStringArray(left, right) {
14881488
&& left.every((value, index) => value === right[index]);
14891489
}
14901490

1491-
function setupHostMatchesCommittedStarterPlan(hostConfig, devices) {
1491+
function setupHostMatchesCommittedStarterPlan(hostConfig, devices, inventory) {
14921492
if (hostConfig.pendingRetirements.length > 0 || hostConfig.aliases.length !== 6) {
14931493
return false;
14941494
}
14951495
const iosVersion = hostConfig.aliases[0]?.iosVersion;
14961496
if (!iosVersion || hostConfig.aliases.some((alias) => alias.iosVersion !== iosVersion)) {
14971497
return false;
14981498
}
1499+
const runtimeIdentifiers = new Set(devices.map((device) => device.runtimeIdentifier));
1500+
if (runtimeIdentifiers.size !== 1) {
1501+
return false;
1502+
}
1503+
const runtimeIdentifier = [...runtimeIdentifiers][0];
1504+
let runtime;
1505+
let setupDeviceTypes;
1506+
try {
1507+
runtime = selectRuntimeForSetup(inventory, iosVersion);
1508+
if (runtime.identifier !== runtimeIdentifier || runtime.version !== iosVersion) {
1509+
return false;
1510+
}
1511+
setupDeviceTypes = {
1512+
iPad: selectPreferredDeviceType(runtime, "iPad").identifier,
1513+
iPhone: selectPreferredDeviceType(runtime, "iPhone").identifier,
1514+
};
1515+
} catch {
1516+
return false;
1517+
}
14991518
const { templates } = buildStarterAliasTemplates({ hostId: hostConfig.hostId, iosVersion });
15001519
const aliasesById = new Map(hostConfig.aliases.map((alias) => [alias.alias, alias]));
15011520
const devicesByAlias = new Map(devices.map((device) => [device.alias, device]));
@@ -1509,7 +1528,10 @@ function setupHostMatchesCommittedStarterPlan(hostConfig, devices) {
15091528
&& alias.resetPolicy === template.resetPolicy
15101529
&& sameStringArray(alias.capabilities, template.capabilities)
15111530
&& device.simulatorId === alias.simulatorId
1512-
&& device.simulatorName === template.simulatorName;
1531+
&& device.simulatorName === template.simulatorName
1532+
&& device.runtimeIdentifier === runtime.identifier
1533+
&& device.runtimeVersion === runtime.version
1534+
&& device.deviceTypeIdentifier === setupDeviceTypes[template.deviceFamily];
15131535
});
15141536
}
15151537

@@ -1632,7 +1654,7 @@ function setupExistingHostState(paths, inventory, options = {}) {
16321654
});
16331655
if (registryMissing) {
16341656
const canResumeRegistryInitialization = issues.length === 0
1635-
&& setupHostMatchesCommittedStarterPlan(hostConfig, devices)
1657+
&& setupHostMatchesCommittedStarterPlan(hostConfig, devices, inventory)
16361658
&& !setupStateContainsLeaseOrPinRecords(paths);
16371659
registryInitializationRequired = canResumeRegistryInitialization;
16381660
issues.push(canResumeRegistryInitialization

broker-core/test/broker-core.test.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,12 +1061,20 @@ test("setup accepts major-only requirements but does not recreate an arbitrary m
10611061
simctlAdapter: paths.simctl.adapter,
10621062
});
10631063
const hostConfig = readJson(paths.hostConfigPath);
1064+
const registry = readJson(resolvedPaths.registryPath);
10641065
hostConfig.aliases = hostConfig.aliases.map((alias) => ({ ...alias, iosVersion: "18" }));
10651066
writeJson(paths.hostConfigPath, hostConfig);
10661067

10671068
const healthy = previewSetupBroker(resolvedPaths, { simctlAdapter: paths.simctl.adapter });
10681069
assert.notEqual(healthy.status, "blocked");
10691070

1071+
fs.rmSync(resolvedPaths.registryPath);
1072+
const ambiguousMissingRegistry = previewSetupBroker(resolvedPaths, { simctlAdapter: paths.simctl.adapter });
1073+
assert.equal(ambiguousMissingRegistry.status, "blocked");
1074+
assert.ok(ambiguousMissingRegistry.prerequisites.some((issue) =>
1075+
issue.id === "registry" && issue.status === "blocked"));
1076+
writeJson(resolvedPaths.registryPath, registry);
1077+
10701078
hostConfig.aliases[0].displayName = "Custom Manual iPhone";
10711079
writeJson(paths.hostConfigPath, hostConfig);
10721080
fs.rmSync(resolvedPaths.registryPath);
@@ -1081,6 +1089,58 @@ test("setup accepts major-only requirements but does not recreate an arbitrary m
10811089
assert.equal(fs.existsSync(resolvedPaths.registryPath), false);
10821090
});
10831091

1092+
test("setup missing-registry recovery requires the selected runtime and device types", () => {
1093+
const paths = makePaths();
1094+
const resolvedPaths = brokerPaths(paths);
1095+
const preview = previewSetupBroker(resolvedPaths, {
1096+
hostId: "registry-identity",
1097+
simctlAdapter: paths.simctl.adapter,
1098+
});
1099+
applySetupBroker(resolvedPaths, {
1100+
confirmPlanId: preview.planId,
1101+
hostId: "registry-identity",
1102+
simctlAdapter: paths.simctl.adapter,
1103+
});
1104+
fs.rmSync(resolvedPaths.registryPath);
1105+
1106+
const state = readJson(paths.simctl.statePath);
1107+
const selectedRuntimeId = state.runtimes[0].identifier;
1108+
const alternateRuntimeId = `${selectedRuntimeId}-Alternate`;
1109+
state.runtimes.push({ ...state.runtimes[0], identifier: alternateRuntimeId });
1110+
state.devices = state.devices.map((device) => ({
1111+
...device,
1112+
runtimeIdentifier: alternateRuntimeId,
1113+
}));
1114+
writeJson(paths.simctl.statePath, state);
1115+
1116+
const runtimeMismatch = previewSetupBroker(resolvedPaths, { simctlAdapter: paths.simctl.adapter });
1117+
assert.equal(runtimeMismatch.status, "blocked");
1118+
assert.ok(runtimeMismatch.prerequisites.some((issue) =>
1119+
issue.id === "registry" && issue.status === "blocked"));
1120+
1121+
const nonPreferredDeviceType = {
1122+
identifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation",
1123+
name: "iPhone SE (3rd generation)",
1124+
productFamily: "iPhone",
1125+
};
1126+
state.devicetypes.push(nonPreferredDeviceType);
1127+
state.runtimes[0].supportedDeviceTypes.push(nonPreferredDeviceType);
1128+
const configuredSimulatorId = readJson(paths.hostConfigPath).aliases[0].simulatorId;
1129+
state.devices = state.devices.map((device) => ({
1130+
...device,
1131+
deviceTypeIdentifier: device.udid === configuredSimulatorId
1132+
? nonPreferredDeviceType.identifier
1133+
: device.deviceTypeIdentifier,
1134+
runtimeIdentifier: selectedRuntimeId,
1135+
}));
1136+
writeJson(paths.simctl.statePath, state);
1137+
1138+
const deviceTypeMismatch = previewSetupBroker(resolvedPaths, { simctlAdapter: paths.simctl.adapter });
1139+
assert.equal(deviceTypeMismatch.status, "blocked");
1140+
assert.ok(deviceTypeMismatch.prerequisites.some((issue) =>
1141+
issue.id === "registry" && issue.status === "blocked"));
1142+
});
1143+
10841144
test("setup resumes registry initialization after a post-commit inventory failure", () => {
10851145
const paths = makePaths();
10861146
const resolvedPaths = brokerPaths(paths);

client/bin/simbroker.mjs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,29 @@ function setupInterruptedError(signalName, completedStages, hostCommitted, faile
837837
});
838838
}
839839

840+
function assertSetupCommittedHostIdentity(snapshot, expectedHostIdentity) {
841+
const expectedSimulatorIds = new Map(
842+
(expectedHostIdentity?.simulators ?? []).map(({ alias, simulatorId }) => [alias, simulatorId]),
843+
);
844+
const actualSimulatorIds = new Map(
845+
(snapshot.simulators ?? []).map(({ alias, simulatorId }) => [alias, simulatorId]),
846+
);
847+
const mismatchedSimulatorAliases = [...new Set([
848+
...expectedSimulatorIds.keys(),
849+
...actualSimulatorIds.keys(),
850+
])]
851+
.filter((alias) => actualSimulatorIds.get(alias) !== expectedSimulatorIds.get(alias))
852+
.sort();
853+
if (snapshot.hostId !== expectedHostIdentity?.hostId || mismatchedSimulatorAliases.length > 0) {
854+
throw new BrokerError("The refreshed snapshot no longer matches the host committed by setup.", {
855+
actualHostId: snapshot.hostId ?? null,
856+
expectedHostId: expectedHostIdentity?.hostId ?? null,
857+
mismatchedSimulatorAliases,
858+
reasonCode: "setup-committed-host-mismatch",
859+
});
860+
}
861+
}
862+
840863
function decorateSetupFailure(error, stage, completedStages, hostCommitted, serviceRunning) {
841864
if (error instanceof BrokerError) {
842865
error.payload.command = "setup";
@@ -917,26 +940,7 @@ async function applySetup(paths, options, cancellation) {
917940
});
918941
}
919942
const expectedHostIdentity = coreResult.setupCommittedHostIdentity;
920-
const expectedSimulatorIds = new Map(
921-
(expectedHostIdentity?.simulators ?? []).map(({ alias, simulatorId }) => [alias, simulatorId]),
922-
);
923-
const actualSimulatorIds = new Map(
924-
(snapshot.simulators ?? []).map(({ alias, simulatorId }) => [alias, simulatorId]),
925-
);
926-
const mismatchedSimulatorAliases = [...new Set([
927-
...expectedSimulatorIds.keys(),
928-
...actualSimulatorIds.keys(),
929-
])]
930-
.filter((alias) => actualSimulatorIds.get(alias) !== expectedSimulatorIds.get(alias))
931-
.sort();
932-
if (snapshot.hostId !== expectedHostIdentity?.hostId || mismatchedSimulatorAliases.length > 0) {
933-
throw new BrokerError("The refreshed snapshot no longer matches the host committed by setup.", {
934-
actualHostId: snapshot.hostId ?? null,
935-
expectedHostId: expectedHostIdentity?.hostId ?? null,
936-
mismatchedSimulatorAliases,
937-
reasonCode: "setup-committed-host-mismatch",
938-
});
939-
}
943+
assertSetupCommittedHostIdentity(snapshot, expectedHostIdentity);
940944
completedStages.push("snapshot");
941945
} catch (error) {
942946
checkCancellation();
@@ -957,6 +961,14 @@ async function applySetup(paths, options, cancellation) {
957961
signal: cancellation.signal,
958962
});
959963
checkCancellation();
964+
const postDoctorSnapshot = JSON.parse(fs.readFileSync(paths.appSnapshotPath, "utf8"));
965+
if (path.resolve(postDoctorSnapshot.hostConfigPath) !== path.resolve(paths.hostConfigPath)
966+
|| path.resolve(postDoctorSnapshot.stateRoot) !== path.resolve(paths.stateRoot)) {
967+
throw new BrokerError("The post-doctor snapshot belongs to different broker paths.", {
968+
reasonCode: "service-identity-mismatch",
969+
});
970+
}
971+
assertSetupCommittedHostIdentity(postDoctorSnapshot, coreResult.setupCommittedHostIdentity);
960972
const expectedAliases = new Set(["manual-1", "ui-1", "ui-2", "build-1", "build-2", "ipad-1"]);
961973
const snapshotAliases = new Set((snapshot.simulators ?? []).map((simulator) => simulator.alias));
962974
const missingExpectedAliases = coreResult.host.created

spec/tasks/guided-simbroker-setup.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Apply performs, in order:
147147
6. `brokerd` start or identity validation
148148
7. fresh snapshot through the active service
149149
8. doctor through the active service
150-
9. verification of path identity, the exact committed host ID and
150+
9. post-doctor snapshot revalidation of path identity, the exact committed host ID and
151151
alias-to-Simulator-ID mapping, expected fresh aliases, matching available
152152
Simulator records, no `repair-needed`/`repairing`, and snapshot path identity
153153
10. `ready` completion
@@ -159,9 +159,10 @@ Apply performs, in order:
159159
- After host commit, preserve host and devices; rerun `simbroker setup`. When
160160
the host commit succeeded but initial registry persistence did not, setup
161161
previews registry initialization as safe finishing work only when the host is
162-
attributable to the canonical starter shape and no lease, pin, or pending
163-
retirement state exists, then reconstructs it from the committed host and
164-
Simulator state during apply.
162+
attributable to the canonical starter shape, every alias records the exact
163+
runtime version used by its Simulator, every Simulator uses the setup-selected
164+
runtime and device type, and no lease, pin, or pending retirement state exists,
165+
then reconstructs it from the committed host and Simulator state during apply.
165166
- Service failure preserves host and reports log path and exact retry.
166167
- Health failure preserves host/service and reports doctor issues plus exact
167168
per-alias repair commands.
@@ -380,5 +381,6 @@ long-running plan/handoff/evaluation, and a passing `agent:complete`.
380381

381382
| Version | Date | Author | Changes |
382383
|---|---|---|---|
384+
| 1.0.2 | 2026-08-23 | `spec-steward`, `ios-dev` | Required exact setup-selected runtime/device identity for registry recovery and post-doctor committed-host revalidation |
383385
| 1.0.1 | 2026-08-23 | `spec-steward`, `ios-dev` | Clarified registry recovery, concurrent setup waits, finishing-stage cancellation, schema rejection, and exact committed-host verification |
384386
| 1.0.0 | 2026-08-22 | `spec-steward`, `ios-dev` | Active guided setup implementation contract |

0 commit comments

Comments
 (0)