Skip to content
Source
Sign inSign up

[Bug]: Device panel reports unknown iOS simulator settings as Default and off #35983

Send to my agent
Closedcoygeek(GitHub profile, opens in a new tab) opened this issue 1 comment

Summary

When an iOS simulator cannot report its text size or Increase Contrast setting, the Device panel presents ordinary settings instead of preserving the unknown state. The server converts both unknown and unsupported responses into Default text size and contrast off. This was reproduced at the production settings-reader boundary using the states documented by the installed Apple tool; no simulator was changed.

Before submitting

  • Related issues: none found for this behavior in searches of open and closed issues and pull requests; nearby history is listed under Additional context.
  • Removed credentials, tokens, private paths, and sensitive data.

Area

apps/server

Observed behavior

With successful subprocess responses containing unknown\n or unsupported\n for simctl ui <device> content_size and increase_contrast, readDeviceDetail returns {"textSize":"default","increaseContrast":false}. For the same sentinel responses, appearance is correctly omitted. A known-value control returns dark appearance, large text size, and contrast enabled as expected. The subprocess boundary was mocked; the production module and its pinned Effect runtime executed directly.

The source connects these values to the Device panel: apps/server/src/device/DeviceService.ts#L1013-L1024 returns the reader's settings, and apps/web/src/components/device/DeviceToolsPanel.tsx#L183-L190 and apps/web/src/components/device/DeviceToolsPanel.tsx#L240-L245 bind them to the text-size choice and contrast switch. The switch disables itself only for an absent value, so converting an unsupported setting to false also makes it appear operable.

Reproduction

  1. Use a clean checkout of the commit in Version on macOS with Node supporting TypeScript imports and synchronous module hooks, npm, Git, and Xcode command-line tools available.
  2. Save the script below as repro.mjs outside the checkout and run node repro.mjs /tmp/t3code-checkout.
  3. Observe the known-value control pass, followed by both documented sentinel inputs returning concrete settings and exit status 1.

The final standalone script reproduced both sentinel cases deterministically in one invocation. It creates a disposable directory inside the checkout, isolates HOME, TMPDIR, npm cache, and T3 state, installs only the lockfile's Effect version with lifecycle scripts and audit submission disabled, and removes that directory on exit. It imports the actual DeviceActions module and replaces only its two error constructors; all subprocess results are fixtures. Its only Apple operations are version/help reads. Exit 0 means the unknown settings stay absent, exit 1 means the defect reproduced, and exit 2 means setup or the known-value control failed.

Reproduction script: repro.mjs
import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
import path from 'node:path';
import { createRequire, registerHooks } from 'node:module';
import { pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';

const checkout = path.resolve(process.argv[2] ?? '.');
let scratch;
let result = 2;
let hooks;
const savedEnv = { ...process.env };
const run = (command, args, options = {}) => {
  const child = spawnSync(command, args, { encoding: 'utf8', ...options });
  if (child.error || child.status !== 0) throw new Error(`${command} failed: ${child.error?.message ?? child.stderr}`);
  return (child.stdout || child.stderr).trim();
};
try {
  console.log(`Node ${process.version}`);
  console.log(run('git', ['--version']));
  console.log(`T3 commit ${run('git', ['-C', checkout, 'rev-parse', 'HEAD'])}`);
  console.log(`npm ${run('npm', ['--version'])}`);
  console.log(run('xcodebuild', ['-version']));
  const help = run('xcrun', ['simctl', 'help', 'ui']);
  if (!help.includes('unknown') || !help.includes('unsupported')) throw new Error('simctl ui reference lacks sentinel states');
  scratch = mkdtempSync(path.join(checkout, '.simctl-probe-'));
  for (const dir of ['home', 'tmp', 'state', 'runtime']) mkdirSync(path.join(scratch, dir));
  process.env.HOME = path.join(scratch, 'home');
  process.env.TMPDIR = path.join(scratch, 'tmp');
  process.env.T3CODE_HOME = path.join(scratch, 'state');
  process.env.npm_config_cache = path.join(scratch, 'npm-cache');
  const lock = readFileSync(path.join(checkout, 'pnpm-lock.yaml'), 'utf8');
  const effectVersion = lock.match(/^  effect: ([^\s]+)$/m)?.[1];
  if (!effectVersion) throw new Error('Effect pin not found');
  run('npm', ['install', '--prefix', path.join(scratch, 'runtime'), '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock', `effect@${effectVersion}`], { cwd: scratch });
  const require = createRequire(path.join(scratch, 'runtime', 'package.json'));
  console.log(`effect ${JSON.parse(readFileSync(require.resolve('effect/package.json'), 'utf8')).version}`);
  // Only the two error constructors are shimmed. The production module, Effect,
  // Schema, generators and readback mapping execute from their actual files.
  const errorShim = 'data:text/javascript,' + encodeURIComponent(`
    import * as Effect from 'effect/Effect';
    export class DeviceOperationError {
      constructor(fields) { Object.assign(this, fields); this._tag = 'DeviceOperationError'; }
      *[Symbol.iterator]() { return yield* Effect.fail(this); }
    }
    export class DeviceActionUnavailableError {
      constructor(fields) { Object.assign(this, fields); this._tag = 'DeviceActionUnavailableError'; }
      *[Symbol.iterator]() { return yield* Effect.fail(this); }
    }
  `);
  const effectUrls = new Map(['effect/Effect', 'effect/Schema', 'effect/Result'].map(specifier => [specifier, pathToFileURL(require.resolve(specifier)).href]));
  hooks = registerHooks({ resolve(specifier, context, nextResolve) {
    if (specifier === '@t3tools/contracts') return { url: errorShim, shortCircuit: true };
    if (effectUrls.has(specifier)) return { url: effectUrls.get(specifier), shortCircuit: true };
    return nextResolve(specifier, context);
  } });
  const Effect = await import('effect/Effect');
  const { readDeviceDetail } = await import(pathToFileURL(path.join(checkout, 'apps/server/src/device/DeviceActions.ts')).href);
  const calls = [];
  let values = { appearance: 'dark', content_size: 'extra-extra-large', increase_contrast: 'enabled' };
  const ready = {
    nodePath: process.execPath,
    hub: { origin: 'http://127.0.0.1:1' },
    devices: {},
    helpers: { serveSimAxSettings: '/fixture/serve-sim-ax-settings' },
    run(command, args, options) {
      calls.push({ command, args, ...(options?.stdin === undefined ? {} : { stdin: options.stdin }) });
      const stdout = args[1] === 'ui' ? `${values[args[3]]}\n` : args[1] === 'spawn' ? '{}' : '';
      return Effect.succeed({ code: 0, stderr: '', stdout });
    },
  };
  const deviceId = '00000000-0000-0000-0000-000000000001';
  const control = await Effect.runPromise(readDeviceDetail(ready, 'ios', deviceId));
  if (JSON.stringify(control.settings) !== JSON.stringify({ appearance: 'dark', textSize: 'large', increaseContrast: true })) throw new Error('known-value control failed');
  console.log(`known-value control ${JSON.stringify(control.settings)}`);
  let broken = false;
  for (const sentinel of ['unknown', 'unsupported']) {
    values = { appearance: sentinel, content_size: sentinel, increase_contrast: sentinel };
    const detail = await Effect.runPromise(readDeviceDetail(ready, 'ios', deviceId));
    console.log(`${sentinel} -> ${JSON.stringify(detail.settings)}`);
    broken ||= detail.settings.textSize !== undefined || detail.settings.increaseContrast !== undefined;
  }
  result = broken ? 1 : 0;
} catch (error) {
  // Do not print machine-local stack paths or registry configuration.
  console.error(`probe setup failed: ${error.message.split('\n')[0]}`);
} finally {
  hooks?.deregister();
  process.env = savedEnv;
  if (scratch) rmSync(scratch, { recursive: true, force: true });
}
console.log(result === 0 ? 'RESULT: expected behavior' : result === 1 ? 'RESULT: defect reproduced' : 'RESULT: inconclusive');
process.exitCode = result;

Expected behavior

Unknown or unsupported readbacks must remain unknown instead of asserting a default text size or a disabled contrast setting. Local xcrun simctl help ui explicitly lists unknown and unsupported for all three read options, separately from standard/accessibility content-size names and enabled/disabled contrast values. This help was observed from Xcode 27.3, build 27C5020n. The existing UI already represents an absent text-size value as Unknown in apps/web/src/components/device/DeviceToolsPanel.tsx#L384-L390 and disables an absent switch value in apps/web/src/components/device/DeviceToolsPanel.tsx#L354-L361; the appearance reader and the existing command-failure test likewise preserve absence rather than fabricating a normal state.

Impact and scope

Form impact: Minor bug or occasional failure.

Impact: P2 - The settings API reports false accessibility state for documented successful simctl responses, and the Device panel consequently presents Default/off and can enable a control for an unsupported setting. This is bounded incorrect state reporting, not a loss of device data or a failure of the primary coding workflow.

The affected inputs are unknown and unsupported content-size and contrast readbacks. Normal named content sizes and enabled/disabled contrast values are unaffected by this finding. No device-level workaround was exercised. NOT_ENOUGH_INFO: no live simulator/runtime was made to emit these sentinel states; a read-only observation from such a runtime would establish their occurrence and frequency on real devices. No full UI session or simulator mutation was performed.

Version

T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93. The reproduction uses the lockfile's effect@4.1.3 and imports DeviceActions directly from that checkout.

Runtime and platform

Observed macOS 27.2, Darwin arm64; Node v26.12.1; npm 12.2.1; Git 2.56.0; Xcode 27.3, build 27C5020n. Simctl is bundled with that Xcode; its help supplies the fixture contract.

Interface

API: device.detail and the detail returned by device.action; Device panel settings in the web/desktop client.

Earliest failure stage

Output or presentation

Relevant output or evidence

Actual output from the final script:

Node v26.12.1
git version 2.56.0
T3 commit 59e64eef881632a23f89c4138bcf37f23d92bf93
npm 12.2.1
Xcode 27.3
Build version 27C5020n
effect 4.1.3
known-value control {"appearance":"dark","textSize":"large","increaseContrast":true}
unknown -> {"textSize":"default","increaseContrast":false}
unsupported -> {"textSize":"default","increaseContrast":false}
RESULT: defect reproduced

xcrun simctl help ui lists the following readback alternatives: appearance can print light, dark, unsupported, or unknown; increase_contrast can print enabled, disabled, unsupported, or unknown; content_size lists standard sizes, extended accessibility sizes, and the other values unknown and unsupported. This is a paraphrase of the locally observed reference, not a captured simulator response.

Source evidence: apps/server/src/device/DeviceActions.ts#L99-L106 falls through to Default for both sentinel content-size strings; apps/server/src/device/DeviceActions.ts#L487-L506 accepts every nonempty contrast string and treats anything except enabled as false. The same code accepts only light/dark appearance values. The production boundary test in apps/server/src/device/DeviceActions.test.ts already expects unreadable settings to produce an empty settings object.

Initial harness setup attempts exited 2 before invoking the production reader because help output was on stderr and then because import resolution recursed. Those setup faults were corrected before the final standalone script above. They supply no device-behavior evidence.

Privacy: the UUID and helper path in the script are synthetic fixtures. The report contains no collected simulator identifiers, account data, credentials, or machine-local home paths.

Analysis or suspected cause

Source: the content-size fallback treats every unmatched nonempty string as a normal category, and the contrast readback checks only equality with enabled. Both collapse Apple's explicit sentinel states into ordinary settings.

Additional context

Searches covered open and closed issues and PRs for simctl, DeviceActions, simulator text size, Increase Contrast, unknown/unsupported settings, and authored device issues. No matching report was found. The nearby text-size history concerns the valid smallest size and does not cover sentinel responses:

  • https://github.com/pingdotgg/t3code/pull/22114 — handles extra-small mapping; the unknown/unsupported readback requirement remains distinct.
  1. tanstack-source added labelsbug

  2. juliusmarmingecommented

    I tried to reproduce this on the current build and could not.

    On origin/main e018563669 I added an it.effect.each case for 'unknown' and 'unsupported' to the existing readDeviceDetail block in apps/server/src/device/DeviceActions.test.ts. The fake host answers every simctl ui <udid> <option> read with <sentinel>\n and fails the accessibility helper. The test calls readDeviceDetail(ready, 'ios', udid) and expects settings to be {}. vp test run src/device/DeviceActions.test.ts -t readDeviceDetail in apps/server: both new cases fail with received { textSize: 'default', increaseContrast: false }. The existing readDeviceDetail tests still pass. Appearance stays absent as expected. I did not see it in a real app run: this VM has no iOS simulator, and I found no way to make a pool simulator report these values, so the evidence is the production code path driven with fake simctl output, as in the reporter's script.

    Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.

  3. juliusmarminge closed this

Sign in to join this conversation.