mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
158 lines
6.0 KiB
TypeScript
158 lines
6.0 KiB
TypeScript
// Physical-device E2E lane.
|
|
//
|
|
// Fast host/device gates run with GSTACK_HAS_IOS_DEVICE=1. The signed build,
|
|
// install, launch, CoreDevice tunnel, and 5x5 live loop are invoked only with
|
|
// the stronger GSTACK_IOS_DEVICE_DEPLOY=1 opt-in.
|
|
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'child_process';
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import {
|
|
PHYSICAL_DEVICE_BUNDLE_ID,
|
|
REQUIRED_LIVE_ITERATIONS,
|
|
TEAM_ID_ENV,
|
|
classifyXcodebuildFailure,
|
|
parseDeviceListPayload,
|
|
redactDeviceForEvidence,
|
|
renderProjectSpec,
|
|
resolveTeamId,
|
|
runPreflightOnly,
|
|
selectPhysicalDevice,
|
|
type PhysicalDevice,
|
|
} from '../ios-qa/scripts/physical-device-smoke';
|
|
|
|
const ROOT = join(import.meta.dir, '..');
|
|
const FIXTURE_PATH = join(ROOT, 'test/fixtures/ios-qa/FixtureApp');
|
|
const HARNESS_PATH = join(ROOT, 'ios-qa/scripts/physical-device-smoke.ts');
|
|
|
|
const DEPLOY = process.env.GSTACK_IOS_DEVICE_DEPLOY === '1';
|
|
const HAS_DEVICE = DEPLOY || process.env.GSTACK_HAS_IOS_DEVICE === '1';
|
|
const describeIfDevice = HAS_DEVICE ? describe : describe.skip;
|
|
const testIfDeploy = DEPLOY ? test : test.skip;
|
|
|
|
const DEVICE_SAMPLE: PhysicalDevice = {
|
|
coreDeviceIdentifier: 'COREDEVICE-UUID',
|
|
hardwareUdid: '00008140-HARDWARE-UDID',
|
|
name: 'Test iPhone',
|
|
model: 'iPhone17,1',
|
|
platform: 'iOS',
|
|
tunnelState: 'connected',
|
|
pairingState: 'paired',
|
|
developerModeStatus: 'enabled',
|
|
transportType: 'wired',
|
|
};
|
|
|
|
describe('physical-device harness invariants', () => {
|
|
test('uses a reserved fixture bundle and requires a real 5/5 run', () => {
|
|
expect(PHYSICAL_DEVICE_BUNDLE_ID).toBe('com.gstack.iosqa.fixture.gstack2');
|
|
expect(REQUIRED_LIVE_ITERATIONS).toBe(5);
|
|
});
|
|
|
|
test('selects the same device by hardware UDID or CoreDevice UUID', () => {
|
|
expect(selectPhysicalDevice([DEVICE_SAMPLE], DEVICE_SAMPLE.hardwareUdid!))
|
|
.toEqual(DEVICE_SAMPLE);
|
|
expect(selectPhysicalDevice([DEVICE_SAMPLE], DEVICE_SAMPLE.coreDeviceIdentifier))
|
|
.toEqual(DEVICE_SAMPLE);
|
|
});
|
|
|
|
test('redacts stable identifiers and the device name from commit-ready evidence', () => {
|
|
const redacted = redactDeviceForEvidence(DEVICE_SAMPLE);
|
|
const serialized = JSON.stringify(redacted);
|
|
expect(redacted.identifierSha256).toHaveLength(64);
|
|
expect(serialized).not.toContain(DEVICE_SAMPLE.coreDeviceIdentifier);
|
|
expect(serialized).not.toContain(DEVICE_SAMPLE.hardwareUdid!);
|
|
expect(serialized).not.toContain(DEVICE_SAMPLE.name);
|
|
});
|
|
|
|
test('preserves typed discovery failures instead of treating bad JSON as no devices', () => {
|
|
expect(() => parseDeviceListPayload({ result: { unexpected: [] } }))
|
|
.toThrow('result.devices');
|
|
});
|
|
|
|
test('temporary project specs never inherit a hardcoded signing team', () => {
|
|
const debugSpec = renderProjectSpec(true);
|
|
const releaseSpec = renderProjectSpec(false);
|
|
expect(debugSpec).toContain('DebugBridgeCore');
|
|
expect(debugSpec).toContain('DebugBridgeUI');
|
|
expect(releaseSpec).not.toContain('DebugBridgeCore');
|
|
expect(releaseSpec).not.toContain('DebugBridgeUI');
|
|
expect(debugSpec).not.toContain('DEVELOPMENT_TEAM');
|
|
expect(releaseSpec).not.toContain('DEVELOPMENT_TEAM');
|
|
});
|
|
|
|
test('accepts only an explicit valid team ID from the harness environment', () => {
|
|
expect(resolveTeamId({})).toBeUndefined();
|
|
expect(resolveTeamId({ [TEAM_ID_ENV]: 'ABCDEFGHIJ' })).toBe('ABCDEFGHIJ');
|
|
expect(() => resolveTeamId({ [TEAM_ID_ENV]: 'not-a-team' }))
|
|
.toThrow('10-character');
|
|
});
|
|
|
|
test('classifies account/provisioning failures as setup gates', () => {
|
|
expect(classifyXcodebuildFailure('Signing for FixtureApp requires a development team.'))
|
|
.toBe('signing_unavailable');
|
|
expect(classifyXcodebuildFailure('error: cannot find value in scope'))
|
|
.toBe('build_failed');
|
|
});
|
|
|
|
test('uses fresh HTTP connections for StateServer close responses', () => {
|
|
const harness = readFileSync(HARNESS_PATH, 'utf8');
|
|
expect(harness).toContain("connection: 'close'");
|
|
});
|
|
|
|
test('fixture exposes a public UIKit accessibility control to the in-process scanner', () => {
|
|
const app = readFileSync(
|
|
join(FIXTURE_PATH, 'Sources/FixtureApp/FixtureAppApp.swift'),
|
|
'utf8',
|
|
);
|
|
expect(app).toContain('struct FixtureButton: UIViewRepresentable');
|
|
expect(app).toContain('button.accessibilityIdentifier = "tap-button"');
|
|
});
|
|
});
|
|
describeIfDevice('ios physical-device path', () => {
|
|
test('Xcode/CoreDevice setup gates pass for one selected wired iPhone', () => {
|
|
const result = runPreflightOnly({
|
|
selector: process.env.GSTACK_IOS_TARGET_UDID,
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
expect(result.device.transportType?.toLowerCase()).toBe('wired');
|
|
expect(result.device.pairingState.toLowerCase()).toBe('paired');
|
|
expect(result.device.developerModeStatus.toLowerCase()).toBe('enabled');
|
|
expect(result.acceptedIdentifiers.coreDeviceIdentifier.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('fixture keeps DebugBridge imports and startup Debug-only', () => {
|
|
const app = readFileSync(
|
|
join(FIXTURE_PATH, 'Sources/FixtureApp/FixtureAppApp.swift'),
|
|
'utf8',
|
|
);
|
|
expect(app).toContain('#if DEBUG');
|
|
expect(app).toContain('import DebugBridgeCore');
|
|
expect(app).toContain('DebugBridgeUIWiring.installAll()');
|
|
expect(app).toContain('#endif');
|
|
});
|
|
|
|
testIfDeploy('builds, signs, installs, launches, and passes five live iterations', () => {
|
|
const result = spawnSync(process.execPath, [HARNESS_PATH, '--json'], {
|
|
cwd: ROOT,
|
|
env: process.env,
|
|
encoding: 'utf8',
|
|
stdio: 'pipe',
|
|
timeout: 30 * 60_000,
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
if (result.status !== 0) {
|
|
console.error(result.stderr || result.stdout);
|
|
}
|
|
expect(result.status).toBe(0);
|
|
const output = JSON.parse(result.stdout) as {
|
|
passedIterations: number;
|
|
requiredIterations: number;
|
|
evidencePath: string;
|
|
};
|
|
expect(output.passedIterations).toBe(5);
|
|
expect(output.requiredIterations).toBe(5);
|
|
expect(existsSync(output.evidencePath)).toBe(true);
|
|
}, 30 * 60_000);
|
|
});
|