Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ task touches:
cloud bridge, or `limrun`.
- Runner/process lease: backend helper mutual-exclusion guard for platform runners or tools; it is
not the remote client ownership boundary.
- Device claim: host-global exclusive ownership of one local device, held by an open session or by a
single sessionless device-mutating command. Local only; remote targets use device leases instead.
- Device-claim policy: required command-descriptor trait declaring a command's relationship to the
claim store (`none`, `observe`, `require-owner`, `transient-exclusive`, `acquire-session`,
`release-session`). The request-execution scope enforces it at the device binding seam:
`transient-exclusive` takes a command-scoped claim and refuses a foreign one, and every other
policy performs no claim-store I/O.
- iOS physical-device control: Apple-local module selected from discovery evidence. CoreDevice
devices retain the `devicectl` controller; devices found only by `xctrace` use the XCTest
controller for readiness, app activation/termination, and cable-bound usbmux runner transport
Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/test-utils/device-claim-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'node:fs';
import path from 'node:path';
import { afterEach } from 'vitest';
import { mkdtempForTestSync } from './tmp-dir.ts';
import type { DeviceClaimReconciler } from '../../daemon/device-claims.ts';

export type IsolatedDeviceClaimStore = {
/** Temporary root holding both the claim store and the daemon state dir. */
root: string;
stateDir: string;
claimsDir: string;
};

/**
* Device claims are host-global by design, so a test that exercises them must
* redirect the store. Registers cleanup once and returns a per-test factory.
*/
export function isolatedDeviceClaimStores(prefix: string): () => IsolatedDeviceClaimStore {
const roots: string[] = [];
afterEach(() => {
delete process.env.AGENT_DEVICE_CLAIMS_DIR;
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
return () => {
const root = mkdtempForTestSync(prefix);
roots.push(root);
const claimsDir = path.join(root, 'claims');
process.env.AGENT_DEVICE_CLAIMS_DIR = claimsDir;
const stateDir = path.join(root, 'state');
fs.mkdirSync(stateDir, { recursive: true });
return { root, stateDir, claimsDir };
};
}

/** Fail-closed reconciler: a test owner is never treated as recoverable. */
export const retainOrphanedDeviceClaims: DeviceClaimReconciler = async () => ({
status: 'retained',
reason: 'test-live-owner',
});
97 changes: 97 additions & 0 deletions src/core/command-descriptor/__tests__/device-claim-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { PUBLIC_COMMANDS } from '../../../command-catalog.ts';
import { commandDescriptors, resolveCommandDeviceClaimPolicy } from '../registry.ts';
import type { DeviceClaimPolicy } from '../types.ts';

// #1320 completeness gate. TypeScript already makes the trait required on every
// raw descriptor, so these tests only pin what types cannot: the CLASSIFICATION,
// and the structural precondition that makes it enforceable.

function commandsByPolicy(): Partial<Record<DeviceClaimPolicy, string[]>> {
const grouped: Partial<Record<DeviceClaimPolicy, string[]>> = {};
for (const descriptor of commandDescriptors) {
(grouped[descriptor.deviceClaimPolicy] ??= []).push(descriptor.name);
}
for (const names of Object.values(grouped)) names.sort();
return grouped;
}

test('every public command resolves the policy its descriptor declares', () => {
const byName = new Map(commandDescriptors.map((descriptor) => [descriptor.name, descriptor]));
for (const command of Object.values(PUBLIC_COMMANDS)) {
const descriptor = byName.get(command);
assert.ok(descriptor, `public command ${command} is missing from the descriptor registry`);
assert.equal(resolveCommandDeviceClaimPolicy(command), descriptor.deviceClaimPolicy);
}
// Command names outside the registry stay claim-free rather than fail closed
// into an acquisition no owner would ever release.
assert.equal(resolveCommandDeviceClaimPolicy(undefined), 'require-owner');
assert.equal(resolveCommandDeviceClaimPolicy('not-a-registered-command'), 'require-owner');
});

test('every command that deviates from require-owner is a reviewed, diffable set', () => {
// CONSERVATIVE: these lists may only change in the same PR that updates them
// here. A `transient-exclusive` command takes host-global exclusive ownership
// of its device for one request, so adding one changes cross-worktree
// behavior for everyone sharing that device (#1799); `none` is for
// host/config-only commands and pure delegators, whose device work runs inside
// the request scope of the command they dispatch.
const { 'require-owner': _sessionBound, ...deviating } = commandsByPolicy();
assert.deepEqual(deviating, {
'acquire-session': ['open'],
'release-session': ['close'],
'transient-exclusive': [
'boot',
'install',
'install_source',
'prepare',
'push',
'reinstall',
'shutdown',
],
observe: ['apps', 'appstate', 'capabilities', 'device', 'devices', 'doctor'],
none: [
'artifacts',
'auth',
'batch',
'cdp',
'connect',
'connection',
'daemon',
'debug',
'disconnect',
'install-from-source',
'lease_allocate',
'lease_heartbeat',
'lease_release',
'mcp',
'metro',
'proxy',
'react-devtools',
'release_materialized_paths',
'session',
'session_list',
'session_save_script',
'web',
],
});
});

test('a transient-exclusive command can actually reach the device binding seam', () => {
// The claim gate lives on the request scope's device binding, which only ADR
// 0019 `device-runtime` commands pass through: an unmigrated (`legacy`)
// command reaches its device through dispatch instead, so declaring
// `transient-exclusive` there would be a claim nobody ever acquires. Live
// verification of #1799 caught exactly that on `keyboard`. Those commands stay
// `require-owner` until their platform execution migrates.
for (const descriptor of commandDescriptors) {
if (descriptor.deviceClaimPolicy !== 'transient-exclusive') continue;
assert.ok(descriptor.daemon, `${descriptor.name}: transient-exclusive without a daemon route`);
assert.equal(
descriptor.platformExecution.kind,
'device-runtime',
`${descriptor.name}: transient-exclusive cannot be enforced without device-runtime execution`,
);
}
});
Loading
Loading