mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
refactor: colocate native platform sources under android/, apple/, linux/ (#1273)
Move the scattered root-level native projects into per-platform folders and drop
the now-redundant platform prefix:
- android-ime-helper/ -> android/ime-helper/
- android-multitouch-helper/ -> android/multitouch-helper/
- android-snapshot-helper/ -> android/snapshot-helper/
- apple-runner/ -> apple/runner/
- macos-helper/ -> apple/macos-helper/
- src/platforms/linux/atspi-dump.py -> linux/atspi-dump.py
Only repo source paths move. Identity surfaces stay frozen so no user's runner
cache is invalidated on upgrade: the derived-cache key hashes source paths
relative to AgentDeviceRunner and excludes packageVersion, and the
~/.agent-device/{apple-runner,macos-helper} namespaces, the
agent-device-android-*-helper artifact/manifest/protocol names, the
AgentDeviceRunner Xcode project, and the `prepare ios-runner` CLI command are
unchanged. Updates build/package scripts, CI, package.json files+scripts,
ignore/attr/fallow configs, runtime path resolvers, and test fixtures.
Also: re-base repo-root-relative refs inside the moved apple/runner for the
added nesting level (gated XCUITest fixture walk + two doc links), and clean the
legacy dist/apple-runner packaged output so the relocated runner can't
double-ship into the wholesale-included dist (with a regression test).
This commit is contained in:
committed by
GitHub
parent
37895caf99
commit
e58cbcdb5f
+1
-1
@@ -28,7 +28,7 @@
|
||||
"examples/test-app/**",
|
||||
"scripts/perf/**",
|
||||
"scripts/layering/**",
|
||||
"apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan",
|
||||
"apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan",
|
||||
"scripts/write-xcuitest-cache-metadata.mjs"
|
||||
],
|
||||
"ignoreDependencies": ["@theme"],
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
android-snapshot-helper/debug.keystore binary
|
||||
android/snapshot-helper/debug.keystore binary
|
||||
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
id: source-hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "value=${{ hashFiles('apple-runner/**', 'scripts/build-xcuitest-apple.sh', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'src/platforms/ios/apple-runner-platform.ts', 'src/platforms/ios/runner-icon.ts', 'src/platforms/ios/runner-xctestrun.ts', 'src/platforms/ios/runner-xctestrun-products.ts', '.github/actions/setup-apple-replay/action.yml', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT"
|
||||
echo "value=${{ hashFiles('apple/runner/**', 'scripts/build-xcuitest-apple.sh', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'src/platforms/ios/apple-runner-platform.ts', 'src/platforms/ios/runner-icon.ts', 'src/platforms/ios/runner-xctestrun.ts', 'src/platforms/ios/runner-xctestrun-products.ts', '.github/actions/setup-apple-replay/action.yml', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
- name: Cache replay prebuilt
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Disallow trailing commas before closing parenthesis in Swift
|
||||
run: |
|
||||
if rg -nU --glob '*.swift' ',\s*\n\s*\)' apple-runner; then
|
||||
if rg -nU --glob '*.swift' ',\s*\n\s*\)' apple/runner; then
|
||||
echo "Found trailing commas before ')' in Swift files. This syntax requires Swift 6.1+ and breaks older Xcode toolchains."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
echo "=== AT-SPI2 Python bindings ==="
|
||||
python3 -c "import gi; gi.require_version('Atspi', '2.0'); from gi.repository import Atspi; print('OK')"
|
||||
echo "=== AT-SPI2 tree dump (quick test) ==="
|
||||
python3 src/platforms/linux/atspi-dump.py --surface desktop --max-nodes 5 | python3 -m json.tool | head -20 || echo "::warning::AT-SPI2 tree dump returned no nodes (expected before any app is launched)"
|
||||
python3 linux/atspi-dump.py --surface desktop --max-nodes 5 | python3 -m json.tool | head -20 || echo "::warning::AT-SPI2 tree dump returned no nodes (expected before any app is launched)"
|
||||
echo "=== xdotool ==="
|
||||
xdotool version
|
||||
|
||||
|
||||
+6
-6
@@ -34,9 +34,9 @@ xcuserdata/
|
||||
*.xctestrun
|
||||
*.xcarchive
|
||||
.skillgym-results/
|
||||
android-snapshot-helper/build/
|
||||
android-snapshot-helper/dist/
|
||||
android-multitouch-helper/build/
|
||||
android-multitouch-helper/dist/
|
||||
android-ime-helper/build/
|
||||
android-ime-helper/dist/
|
||||
android/snapshot-helper/build/
|
||||
android/snapshot-helper/dist/
|
||||
android/multitouch-helper/build/
|
||||
android/multitouch-helper/dist/
|
||||
android/ime-helper/build/
|
||||
android/ime-helper/dist/
|
||||
|
||||
+4
-4
@@ -8,10 +8,10 @@ examples/test-app/.expo/
|
||||
.build/
|
||||
.swiftpm/
|
||||
DerivedData/
|
||||
android-snapshot-helper/build/
|
||||
android-snapshot-helper/dist/
|
||||
android-multitouch-helper/build/
|
||||
android-multitouch-helper/dist/
|
||||
android/snapshot-helper/build/
|
||||
android/snapshot-helper/dist/
|
||||
android/multitouch-helper/build/
|
||||
android/multitouch-helper/dist/
|
||||
|
||||
# Tool caches
|
||||
.fallow/
|
||||
|
||||
@@ -20,7 +20,7 @@ sh ./scripts/build-android-snapshot-helper.sh "$VERSION" .tmp/android-snapshot-h
|
||||
|
||||
The build uses Android SDK command-line tools directly. It expects `ANDROID_HOME` or
|
||||
`ANDROID_SDK_ROOT` to point at an SDK with `platforms/android-36` and matching build tools.
|
||||
`pnpm prepack` builds the npm-bundled helper into `android-snapshot-helper/dist`; npm users get
|
||||
`pnpm prepack` builds the npm-bundled helper into `android/snapshot-helper/dist`; npm users get
|
||||
that APK in the package and the first helper-backed `snapshot` installs it automatically when
|
||||
missing or outdated.
|
||||
|
||||
+2
-1
@@ -53,7 +53,8 @@ extension RunnerTests {
|
||||
let fixtureURL = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent() // AgentDeviceRunnerUITests
|
||||
.deletingLastPathComponent() // AgentDeviceRunner
|
||||
.deletingLastPathComponent() // apple-runner
|
||||
.deletingLastPathComponent() // runner
|
||||
.deletingLastPathComponent() // apple
|
||||
.deletingLastPathComponent() // repo root
|
||||
.appendingPathComponent("contracts")
|
||||
.appendingPathComponent("fixtures")
|
||||
@@ -15,7 +15,7 @@ Current internal runner for iOS, tvOS, and macOS desktop automation.
|
||||
Protocol and maintenance references:
|
||||
|
||||
- Protocol overview: [`RUNNER_PROTOCOL.md`](RUNNER_PROTOCOL.md)
|
||||
- TypeScript client: [`../src/platforms/apple/core/runner/runner-client.ts`](../src/platforms/apple/core/runner/runner-client.ts)
|
||||
- TypeScript client: [`../../src/platforms/apple/core/runner/runner-client.ts`](../../src/platforms/apple/core/runner/runner-client.ts)
|
||||
- Swift wire models: [`AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift`](AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift)
|
||||
|
||||
## UITest Runner File Map
|
||||
@@ -46,7 +46,7 @@ serialize reliably. In those cases interactive snapshots may return a sparse roo
|
||||
full snapshots preserve the XCTest error. A penalized simulator can recover through private AX;
|
||||
physical devices use a short XCTest probe because no non-XCTest semantic backend is available
|
||||
there. See
|
||||
[`../docs/adr/0004-ios-snapshot-backend-strategy.md`](../docs/adr/0004-ios-snapshot-backend-strategy.md)
|
||||
[`../../docs/adr/0004-ios-snapshot-backend-strategy.md`](../../docs/adr/0004-ios-snapshot-backend-strategy.md)
|
||||
for the backend boundary and future simulator AX-service direction.
|
||||
|
||||
## Protocol Notes
|
||||
@@ -41,7 +41,7 @@ Examples:
|
||||
|
||||
The current command names are defined in:
|
||||
|
||||
- [`../src/platforms/apple/core/runner/runner-client.ts`](../src/platforms/apple/core/runner/runner-client.ts)
|
||||
- [`../../src/platforms/apple/core/runner/runner-client.ts`](../../src/platforms/apple/core/runner/runner-client.ts)
|
||||
- [`AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift`](AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift)
|
||||
|
||||
## Response Shape
|
||||
+16
-16
@@ -90,16 +90,16 @@
|
||||
"build:xcuitest:visionos:clean": "pnpm clean:xcuitest:visionos && pnpm build:xcuitest:visionos",
|
||||
"build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
|
||||
"package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
|
||||
"package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist",
|
||||
"package:android-snapshot-helper:npm": "rm -rf android/snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android/snapshot-helper/dist",
|
||||
"package:apple-runner:npm": "node scripts/package-apple-runner-source.mjs",
|
||||
"build:android-multitouch-helper": "sh ./scripts/build-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
|
||||
"package:android-multitouch-helper": "sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
|
||||
"package:android-multitouch-helper:npm": "rm -rf android-multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android-multitouch-helper/dist",
|
||||
"package:android-multitouch-helper:npm": "rm -rf android/multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android/multitouch-helper/dist",
|
||||
"build:android": "pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm && pnpm package:android-ime-helper:npm",
|
||||
"build:android-ime-helper": "sh ./scripts/build-android-ime-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-ime-helper",
|
||||
"package:android-ime-helper": "sh ./scripts/package-android-ime-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-ime-helper",
|
||||
"package:android-ime-helper:npm": "rm -rf android-ime-helper/dist && sh ./scripts/package-android-ime-helper.sh $(node -p \"require('./package.json').version\") android-ime-helper/dist",
|
||||
"build:macos-helper": "swift build -c release --package-path macos-helper",
|
||||
"package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && sh ./scripts/package-android-ime-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist",
|
||||
"build:macos-helper": "swift build -c release --package-path apple/macos-helper",
|
||||
"build:all": "pnpm build:node && pnpm build:xcuitest",
|
||||
"ad": "node bin/agent-device.mjs",
|
||||
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
|
||||
@@ -163,18 +163,18 @@
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"macos-helper",
|
||||
"!macos-helper/**/.build",
|
||||
"android-snapshot-helper/dist",
|
||||
"!android-snapshot-helper/dist/*.idsig",
|
||||
"!android-snapshot-helper/README.md",
|
||||
"android-multitouch-helper/dist",
|
||||
"!android-multitouch-helper/dist/*.idsig",
|
||||
"!android-multitouch-helper/README.md",
|
||||
"android-ime-helper/dist",
|
||||
"!android-ime-helper/dist/*.idsig",
|
||||
"!android-ime-helper/README.md",
|
||||
"src/platforms/linux/atspi-dump.py",
|
||||
"apple/macos-helper",
|
||||
"!apple/macos-helper/**/.build",
|
||||
"android/snapshot-helper/dist",
|
||||
"!android/snapshot-helper/dist/*.idsig",
|
||||
"!android/snapshot-helper/README.md",
|
||||
"android/multitouch-helper/dist",
|
||||
"!android/multitouch-helper/dist/*.idsig",
|
||||
"!android/multitouch-helper/README.md",
|
||||
"android/ime-helper/dist",
|
||||
"!android/ime-helper/dist/*.idsig",
|
||||
"!android/ime-helper/README.md",
|
||||
"linux/atspi-dump.py",
|
||||
"skills",
|
||||
"server.json",
|
||||
"smithery.yaml",
|
||||
|
||||
@@ -9,7 +9,7 @@ fi
|
||||
VERSION="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
PROJECT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
HELPER_DIR="$PROJECT_DIR/android-ime-helper"
|
||||
HELPER_DIR="$PROJECT_DIR/android/ime-helper"
|
||||
PACKAGE_NAME="com.callstack.agentdevice.imehelper"
|
||||
MIN_SDK=23
|
||||
TARGET_SDK=36
|
||||
@@ -49,7 +49,7 @@ BUILD_DIR="$HELPER_DIR/build"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
DEX_DIR="$BUILD_DIR/dex"
|
||||
RES_COMPILED_DIR="$BUILD_DIR/res-compiled"
|
||||
KEYSTORE="$PROJECT_DIR/android-snapshot-helper/debug.keystore"
|
||||
KEYSTORE="$PROJECT_DIR/android/snapshot-helper/debug.keystore"
|
||||
UNSIGNED_APK="$BUILD_DIR/helper-unsigned.apk"
|
||||
ALIGNED_APK="$BUILD_DIR/helper-aligned.apk"
|
||||
APK_PATH="$OUTPUT_DIR/$APK_BASENAME"
|
||||
|
||||
@@ -9,7 +9,7 @@ fi
|
||||
VERSION="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
PROJECT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
HELPER_DIR="$PROJECT_DIR/android-multitouch-helper"
|
||||
HELPER_DIR="$PROJECT_DIR/android/multitouch-helper"
|
||||
PACKAGE_NAME="com.callstack.agentdevice.multitouchhelper"
|
||||
MIN_SDK=23
|
||||
TARGET_SDK=36
|
||||
@@ -49,7 +49,7 @@ BUILD_DIR="$HELPER_DIR/build"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
TEST_CLASSES_DIR="$BUILD_DIR/test-classes"
|
||||
DEX_DIR="$BUILD_DIR/dex"
|
||||
KEYSTORE="$PROJECT_DIR/android-snapshot-helper/debug.keystore"
|
||||
KEYSTORE="$PROJECT_DIR/android/snapshot-helper/debug.keystore"
|
||||
UNSIGNED_APK="$BUILD_DIR/helper-unsigned.apk"
|
||||
ALIGNED_APK="$BUILD_DIR/helper-aligned.apk"
|
||||
APK_PATH="$OUTPUT_DIR/$APK_BASENAME"
|
||||
|
||||
@@ -9,7 +9,7 @@ fi
|
||||
VERSION="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
PROJECT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
HELPER_DIR="$PROJECT_DIR/android-snapshot-helper"
|
||||
HELPER_DIR="$PROJECT_DIR/android/snapshot-helper"
|
||||
PACKAGE_NAME="com.callstack.agentdevice.snapshothelper"
|
||||
MIN_SDK=23
|
||||
TARGET_SDK=36
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -eu
|
||||
|
||||
PLATFORM="${AGENT_DEVICE_XCUITEST_PLATFORM:-}"
|
||||
PROJECT_PATH="apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj"
|
||||
PROJECT_PATH="apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj"
|
||||
SCHEME="AgentDeviceRunner"
|
||||
DEFAULT_IOS_RUNNER_APP_BUNDLE_ID="com.callstack.agentdevice.runner"
|
||||
|
||||
|
||||
@@ -75,13 +75,13 @@ test('android-adb stub test delegates project ownership to Vitest', () => {
|
||||
});
|
||||
|
||||
test('Swift runner change selects the swift-runner build', () => {
|
||||
assert.deepEqual(ids(['apple-runner/Sources/Runner/Main.swift']), ['swift-runner']);
|
||||
assert.deepEqual(ids(['apple/runner/Sources/Runner/Main.swift']), ['swift-runner']);
|
||||
assert.ok(ids(['src/platforms/apple/core/runner/Support.swift']).includes('swift-runner'));
|
||||
});
|
||||
|
||||
test('Android helper change selects the android-helpers build', () => {
|
||||
assert.deepEqual(ids(['android-snapshot-helper/src/Main.kt']), ['android-helpers']);
|
||||
assert.deepEqual(ids(['android-multitouch-helper/build.gradle']), ['android-helpers']);
|
||||
assert.deepEqual(ids(['android/snapshot-helper/src/Main.kt']), ['android-helpers']);
|
||||
assert.deepEqual(ids(['android/multitouch-helper/build.gradle']), ['android-helpers']);
|
||||
});
|
||||
|
||||
test('MCP metadata change selects the mcp-metadata check', () => {
|
||||
|
||||
@@ -244,20 +244,21 @@ const BUILD_OWNERSHIP: ReadonlyArray<{
|
||||
check: 'swift-runner',
|
||||
rule: 'own:swift',
|
||||
detail: 'Swift runner sources require the XCUITest build',
|
||||
owns: (file) => file.startsWith('apple-runner/') || file.endsWith('.swift'),
|
||||
owns: (file) => file.startsWith('apple/runner/') || file.endsWith('.swift'),
|
||||
},
|
||||
{
|
||||
check: 'android-helpers',
|
||||
rule: 'own:android-helpers',
|
||||
detail: 'Android helper packages have their own build',
|
||||
owns: (file) =>
|
||||
file.startsWith('android-snapshot-helper/') || file.startsWith('android-multitouch-helper/'),
|
||||
file.startsWith('android/snapshot-helper/') ||
|
||||
file.startsWith('android/multitouch-helper/'),
|
||||
},
|
||||
{
|
||||
check: 'macos-helper',
|
||||
rule: 'own:macos-helper',
|
||||
detail: 'macOS helper is a separate Swift package build',
|
||||
owns: (file) => file.startsWith('macos-helper/'),
|
||||
owns: (file) => file.startsWith('apple/macos-helper/'),
|
||||
},
|
||||
{
|
||||
check: 'mcp-metadata',
|
||||
|
||||
@@ -4,8 +4,16 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const UNIT_TEST_CONDITION = 'AGENT_DEVICE_RUNNER_UNIT_TESTS';
|
||||
const SOURCE_DIR = path.join('apple-runner');
|
||||
const OUTPUT_DIR = path.join('dist', 'apple-runner');
|
||||
const SOURCE_DIR = path.join('apple', 'runner');
|
||||
const OUTPUT_DIR = path.join('dist', 'apple', 'runner');
|
||||
// Packaged-runner locations from before the apple-runner/ -> apple/runner/ move. `dist` ships
|
||||
// wholesale, so a stale tree left by an older build/checkout would double-ship into the npm
|
||||
// package (and inflate the bundle-size diff, which packages the base then the PR into one dist).
|
||||
// Always remove them so only the current OUTPUT_DIR survives.
|
||||
const LEGACY_OUTPUT_DIRS = [
|
||||
path.join('dist', 'apple-runner'),
|
||||
path.join('dist', 'apple', 'apple-runner'),
|
||||
];
|
||||
const SKIPPED_DIR_NAMES = new Set(['.build', '.swiftpm', 'xcuserdata']);
|
||||
const SKIPPED_ROOT_FILES = new Set(['README.md', 'RUNNER_PROTOCOL.md']);
|
||||
|
||||
@@ -18,6 +26,9 @@ export function packageAppleRunnerSource(options = {}) {
|
||||
}
|
||||
|
||||
fs.rmSync(outputRoot, { recursive: true, force: true });
|
||||
for (const legacyDir of LEGACY_OUTPUT_DIRS) {
|
||||
fs.rmSync(path.join(root, legacyDir), { recursive: true, force: true });
|
||||
}
|
||||
const summary = {
|
||||
outputRoot,
|
||||
copiedFiles: 0,
|
||||
|
||||
@@ -49,7 +49,7 @@ function resolveRunnerTestBundleId() {
|
||||
}
|
||||
|
||||
function computeRunnerSourceFingerprint() {
|
||||
const runnerRoot = path.join(projectRoot, 'apple-runner', 'AgentDeviceRunner');
|
||||
const runnerRoot = path.join(projectRoot, 'apple', 'runner', 'AgentDeviceRunner');
|
||||
const files = collectRunnerSourceFiles(runnerRoot);
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const file of files) {
|
||||
|
||||
@@ -10,28 +10,28 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../
|
||||
const packageScript = path.join(repoRoot, 'scripts', 'package-apple-runner-source.mjs');
|
||||
const runnerSnapshotSwiftPath = path.join(
|
||||
repoRoot,
|
||||
'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift',
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift',
|
||||
);
|
||||
|
||||
test('package apple runner source strips unit-test blocks without mutating checkout source', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-package-'));
|
||||
onTestFinished(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
writeFixtureFile(root, 'apple-runner/README.md', 'developer docs\n');
|
||||
writeFixtureFile(root, 'apple-runner/.build/cache.txt', 'cache\n');
|
||||
writeFixtureFile(root, 'apple/runner/README.md', 'developer docs\n');
|
||||
writeFixtureFile(root, 'apple/runner/.build/cache.txt', 'cache\n');
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj',
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj',
|
||||
'',
|
||||
);
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
|
||||
'state\n',
|
||||
);
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
[
|
||||
'extension RunnerTests {',
|
||||
' func runtimeHelper() {}',
|
||||
@@ -54,11 +54,11 @@ test('package apple runner source strips unit-test blocks without mutating check
|
||||
|
||||
const sourceSwiftPath = path.join(
|
||||
root,
|
||||
'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
);
|
||||
const packagedSwiftPath = path.join(
|
||||
root,
|
||||
'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
'dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
|
||||
);
|
||||
const sourceSwift = fs.readFileSync(sourceSwiftPath, 'utf8');
|
||||
const packagedSwift = fs.readFileSync(packagedSwiftPath, 'utf8');
|
||||
@@ -70,22 +70,52 @@ test('package apple runner source strips unit-test blocks without mutating check
|
||||
assert.match(packagedSwift, /#if os\(macOS\)/);
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
path.join(root, 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj'),
|
||||
path.join(root, 'dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj'),
|
||||
),
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/README.md')), false);
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/.build/cache.txt')), false);
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple/runner/README.md')), false);
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple/runner/.build/cache.txt')), false);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
|
||||
'dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('package apple runner source removes legacy dist/apple-runner output before shipping', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-package-legacy-'));
|
||||
onTestFinished(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
// Minimal current-layout source so packaging succeeds.
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj',
|
||||
'',
|
||||
);
|
||||
// Stale packaged trees left by builds/checkouts predating the apple-runner -> apple/runner
|
||||
// move. `dist` ships wholesale, so these must not survive packaging or they double-ship.
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'dist/apple-runner/AgentDeviceRunner/RunnerTests+Legacy.swift',
|
||||
'legacy\n',
|
||||
);
|
||||
writeFixtureFile(
|
||||
root,
|
||||
'dist/apple/apple-runner/AgentDeviceRunner/RunnerTests+Mid.swift',
|
||||
'mid\n',
|
||||
);
|
||||
|
||||
await runCmd(process.execPath, [packageScript, '--root', root, '--quiet']);
|
||||
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner')), false);
|
||||
assert.equal(fs.existsSync(path.join(root, 'dist/apple/apple-runner')), false);
|
||||
assert.ok(fs.existsSync(path.join(root, 'dist/apple/runner/AgentDeviceRunner')));
|
||||
});
|
||||
|
||||
test('apple runner tree snapshot capture stays on the main queue', () => {
|
||||
const source = fs.readFileSync(runnerSnapshotSwiftPath, 'utf8');
|
||||
const boundedCapture = extractSwiftFunction(source, 'captureSnapshotRootBounded');
|
||||
|
||||
@@ -6,7 +6,8 @@ export const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
|
||||
|
||||
const RUNNER_SOURCES_DIR = path.join(
|
||||
PROJECT_ROOT,
|
||||
'apple-runner',
|
||||
'apple',
|
||||
'runner',
|
||||
'AgentDeviceRunner',
|
||||
'AgentDeviceRunnerUITests',
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
const IOS_RUNNER_XCODEBUILD_KILL_PATTERNS = [
|
||||
'xcodebuild .*AgentDeviceRunnerUITests/RunnerTests/testCommand',
|
||||
'xcodebuild .*AgentDeviceRunner\\.env\\.session-',
|
||||
'xcodebuild build-for-testing .*apple-runner/AgentDeviceRunner/AgentDeviceRunner\\.xcodeproj',
|
||||
'xcodebuild build-for-testing .*apple/runner/AgentDeviceRunner/AgentDeviceRunner\\.xcodeproj',
|
||||
];
|
||||
|
||||
type BoundedTimeoutPolicy = CommandTimeoutPolicy & { envelopeMs: number };
|
||||
|
||||
@@ -6,7 +6,7 @@ const NORMAL_IME = 'com.google.android.inputmethod.latin/.LatinIME';
|
||||
|
||||
// probeAndroidTestIme reads the helper's service component from the bundled artifact; inject a
|
||||
// fixture so the orphan-detection checks pass on a fresh checkout that hasn't packaged
|
||||
// android-ime-helper/dist (CI's Coverage job runs no packaging step).
|
||||
// android/ime-helper/dist (CI's Coverage job runs no packaging step).
|
||||
vi.mock('../../../platforms/android/ime-helper.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/android/ime-helper.ts')>();
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { findProjectRoot } from '../../../utils/version.ts';
|
||||
|
||||
const SERVICE_SRC = path.join(
|
||||
findProjectRoot(),
|
||||
'android-ime-helper/src/main/java/com/callstack/agentdevice/imehelper/TestInputMethodService.java',
|
||||
'android/ime-helper/src/main/java/com/callstack/agentdevice/imehelper/TestInputMethodService.java',
|
||||
);
|
||||
|
||||
function readService(): string {
|
||||
|
||||
@@ -9,7 +9,7 @@ const SETTINGS_KEY = 'agent_device_ime_helper_previous_ime';
|
||||
const PENDING_DIR = 'android-test-ime-pending';
|
||||
|
||||
// activateAndroidTestIme reads the bundled artifact for the service component; inject a fixture so
|
||||
// the suite passes on a fresh checkout that hasn't packaged android-ime-helper/dist (CI Coverage).
|
||||
// the suite passes on a fresh checkout that hasn't packaged android/ime-helper/dist (CI Coverage).
|
||||
vi.mock('../ime-helper.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../ime-helper.ts')>();
|
||||
const fixture = await import('../../../__tests__/test-utils/android-snapshot-helper.ts');
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
|
||||
const PACKAGE = 'com.callstack.agentdevice.imehelper';
|
||||
|
||||
// Inject a fixture artifact so the tests never read android-ime-helper/dist from disk (which a
|
||||
// Inject a fixture artifact so the tests never read android/ime-helper/dist from disk (which a
|
||||
// fresh checkout that hasn't packaged the helper won't have — CI's Coverage job included).
|
||||
vi.mock('../ime-helper.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../ime-helper.ts')>();
|
||||
|
||||
@@ -141,7 +141,7 @@ export async function resolveAndroidHelperArtifact<
|
||||
unavailableMessage: string;
|
||||
}): Promise<{ apkPath: string; manifest: Manifest }> {
|
||||
const version = readVersion();
|
||||
const helperDir = path.join(findProjectRoot(), options.helperDirName, 'dist');
|
||||
const helperDir = path.join(findProjectRoot(), 'android', options.helperDirName, 'dist');
|
||||
const manifestPath = path.join(helperDir, options.manifestFileName(version));
|
||||
try {
|
||||
const manifest = options.parseManifest(JSON.parse(await fs.readFile(manifestPath, 'utf8')));
|
||||
|
||||
@@ -49,7 +49,7 @@ export type AndroidImeHelperArtifact = {
|
||||
|
||||
export async function resolveAndroidImeHelperArtifact(): Promise<AndroidImeHelperArtifact> {
|
||||
return await resolveAndroidHelperArtifact({
|
||||
helperDirName: 'android-ime-helper',
|
||||
helperDirName: 'ime-helper',
|
||||
manifestFileName: (version) => `agent-device-android-ime-helper-${version}.manifest.json`,
|
||||
parseManifest: parseAndroidImeHelperManifest,
|
||||
unavailableMessage:
|
||||
|
||||
@@ -36,7 +36,7 @@ export type AndroidMultiTouchHelperArtifact = {
|
||||
|
||||
export async function resolveAndroidMultiTouchHelperArtifact(): Promise<AndroidMultiTouchHelperArtifact> {
|
||||
return await resolveAndroidHelperArtifact({
|
||||
helperDirName: 'android-multitouch-helper',
|
||||
helperDirName: 'multitouch-helper',
|
||||
manifestFileName: (version) =>
|
||||
`agent-device-android-multitouch-helper-${version}.manifest.json`,
|
||||
parseManifest: parseAndroidMultiTouchHelperManifest,
|
||||
|
||||
@@ -513,7 +513,7 @@ async function resolveAndroidSnapshotHelperArtifact(
|
||||
}
|
||||
|
||||
const version = readVersion();
|
||||
const helperDir = path.join(findProjectRoot(), 'android-snapshot-helper', 'dist');
|
||||
const helperDir = path.join(findProjectRoot(), 'android', 'snapshot-helper', 'dist');
|
||||
const manifestPath = path.join(
|
||||
helperDir,
|
||||
`agent-device-android-snapshot-helper-${version}.manifest.json`,
|
||||
|
||||
@@ -183,7 +183,7 @@ beforeEach(() => {
|
||||
|
||||
test('resolveMacOsHelperPackageRootFrom finds helper package from source and dist-like paths', async () => {
|
||||
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-helper-root-'));
|
||||
const helperRoot = path.join(repoRoot, 'macos-helper');
|
||||
const helperRoot = path.join(repoRoot, 'apple', 'macos-helper');
|
||||
await fs.mkdir(helperRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(helperRoot, 'Package.swift'), '// test\n', 'utf8');
|
||||
|
||||
|
||||
@@ -975,7 +975,7 @@ test('xctestrunReferencesProjectRoot rejects stale worktree artifacts', async ()
|
||||
const xctestrunPath = path.join(tmpDir, 'AgentDeviceRunner.xctestrun');
|
||||
fs.writeFileSync(
|
||||
xctestrunPath,
|
||||
'<plist><dict><key>SourceFilesCommonPathPrefix</key><string>/tmp/other-worktree/agent-device/apple-runner/AgentDeviceRunner</string></dict></plist>',
|
||||
'<plist><dict><key>SourceFilesCommonPathPrefix</key><string>/tmp/other-worktree/agent-device/apple/runner/AgentDeviceRunner</string></dict></plist>',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
@@ -1026,7 +1026,8 @@ test('resolveRunnerDerivedPath reuses cache path for identical runner source fin
|
||||
const firstRoot = path.join(tmpDir, 'first');
|
||||
const secondRoot = path.join(tmpDir, 'second');
|
||||
const runnerRelativePath = path.join(
|
||||
'apple-runner',
|
||||
'apple',
|
||||
'runner',
|
||||
'AgentDeviceRunner',
|
||||
'AgentDeviceRunnerUITests',
|
||||
'RunnerTests.swift',
|
||||
@@ -1283,7 +1284,8 @@ test('ensureXctestrunArtifact rebuilds after cached macOS runner repair failure'
|
||||
const { derivedPath, existingXctestrunPath } = await makeCachedRunnerXctestrun();
|
||||
const projectPath = path.join(
|
||||
projectRoot,
|
||||
'apple-runner',
|
||||
'apple',
|
||||
'runner',
|
||||
'AgentDeviceRunner',
|
||||
'AgentDeviceRunner.xcodeproj',
|
||||
);
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
|
||||
test('resolveAppleRunnerSourceRoot prefers checkout source over packaged source', () => {
|
||||
const root = makeTempRoot();
|
||||
const checkoutSource = path.join(root, 'apple-runner', 'AgentDeviceRunner');
|
||||
const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner');
|
||||
const checkoutSource = path.join(root, 'apple', 'runner', 'AgentDeviceRunner');
|
||||
const packagedSource = path.join(root, 'dist', 'apple', 'runner', 'AgentDeviceRunner');
|
||||
fs.mkdirSync(path.join(checkoutSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true });
|
||||
fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true });
|
||||
|
||||
@@ -24,7 +24,7 @@ test('resolveAppleRunnerSourceRoot prefers checkout source over packaged source'
|
||||
|
||||
test('resolveAppleRunnerSourceRoot falls back to packaged source', () => {
|
||||
const root = makeTempRoot();
|
||||
const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner');
|
||||
const packagedSource = path.join(root, 'dist', 'apple', 'runner', 'AgentDeviceRunner');
|
||||
fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true });
|
||||
|
||||
assert.equal(resolveAppleRunnerSourceRoot(root), packagedSource);
|
||||
|
||||
@@ -215,12 +215,12 @@ test('setup metadata script matches expected iOS simulator cache metadata', asyn
|
||||
const binDir = path.join(root, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
fs.mkdirSync(derivedRoot, { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, 'apple-runner', 'AgentDeviceRunner'), {
|
||||
fs.mkdirSync(path.join(projectRoot, 'apple', 'runner', 'AgentDeviceRunner'), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(path.join(projectRoot, 'package.json'), '{"version":"0.19.0"}\n');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'apple-runner', 'AgentDeviceRunner', 'Runner.swift'),
|
||||
path.join(projectRoot, 'apple', 'runner', 'AgentDeviceRunner', 'Runner.swift'),
|
||||
'final class Runner {}\n',
|
||||
);
|
||||
writeExecutable(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const APPLE_RUNNER_SOURCE_ROOT = path.join('apple-runner', 'AgentDeviceRunner');
|
||||
const PACKAGED_APPLE_RUNNER_SOURCE_ROOT = path.join('dist', 'apple-runner', 'AgentDeviceRunner');
|
||||
const APPLE_RUNNER_SOURCE_ROOT = path.join('apple', 'runner', 'AgentDeviceRunner');
|
||||
const PACKAGED_APPLE_RUNNER_SOURCE_ROOT = path.join('dist', 'apple', 'runner', 'AgentDeviceRunner');
|
||||
|
||||
export function resolveAppleRunnerSourceRoot(projectRoot: string): string {
|
||||
const checkoutSourceRoot = path.join(projectRoot, APPLE_RUNNER_SOURCE_ROOT);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
export type MacOsPermissionTarget = 'accessibility' | 'screen-recording' | 'input-monitoring';
|
||||
|
||||
// Keep this shape aligned with macOS helper SnapshotNodeResponse in
|
||||
// macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift.
|
||||
// apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift.
|
||||
export type MacOsSnapshotNode = {
|
||||
index: number;
|
||||
type?: string;
|
||||
@@ -99,7 +99,7 @@ function appendMacOsHelperContextArgs(
|
||||
export function resolveMacOsHelperPackageRootFrom(modulePath: string): string {
|
||||
let currentDir = path.dirname(modulePath);
|
||||
while (true) {
|
||||
const candidate = path.join(currentDir, 'macos-helper');
|
||||
const candidate = path.join(currentDir, 'apple', 'macos-helper');
|
||||
if (existsSync(path.join(candidate, 'Package.swift'))) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -30,29 +30,21 @@ const SCRIPT_NAME = 'atspi-dump.py';
|
||||
|
||||
let cachedScriptPath: string | null = null;
|
||||
|
||||
/** Resolve atspi-dump.py relative to this module, checking both source and dist layouts. */
|
||||
/** Resolve atspi-dump.py, which lives under `linux/` at the repo/package root. */
|
||||
function resolveScriptPath(): string {
|
||||
if (cachedScriptPath) return cachedScriptPath;
|
||||
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Walk upward looking for the script — handles both:
|
||||
// src/platforms/linux/ (source)
|
||||
// dist/src/ (bundled, .py lives in package root under src/platforms/linux/)
|
||||
// Walk upward looking for linux/atspi-dump.py — handles both:
|
||||
// <repo root>/linux/ (source; this module lives at src/platforms/linux/)
|
||||
// <package root>/linux/ (bundled; this module lives under dist/src/)
|
||||
let dir = thisDir;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const candidate = path.join(dir, 'src', 'platforms', 'linux', SCRIPT_NAME);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = path.join(dir, 'linux', SCRIPT_NAME);
|
||||
if (fs.existsSync(candidate)) {
|
||||
cachedScriptPath = candidate;
|
||||
return candidate;
|
||||
}
|
||||
// Also check same-directory (running from source dir directly)
|
||||
if (i === 0) {
|
||||
const sameDir = path.join(dir, SCRIPT_NAME);
|
||||
if (fs.existsSync(sameDir)) {
|
||||
cachedScriptPath = sameDir;
|
||||
return sameDir;
|
||||
}
|
||||
}
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ test('recording script candidates include packaged dist apple-runner source', ()
|
||||
const packageRoot = path.join(tmpDir, 'package');
|
||||
const scriptPath = path.join(
|
||||
packageRoot,
|
||||
'dist/apple-runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift',
|
||||
'dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
||||
fs.writeFileSync(scriptPath, 'print("overlay")\n');
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getRecordingOverlaySupportWarning } from '../overlay.ts';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const recordingScriptsDir = path.resolve(
|
||||
__dirname,
|
||||
'../../../apple-runner/AgentDeviceRunner/RecordingScripts',
|
||||
'../../../apple/runner/AgentDeviceRunner/RecordingScripts',
|
||||
);
|
||||
const recordingTestSupportDir = path.resolve(__dirname, '../../../test/integration/support');
|
||||
const SWIFT_TYPECHECK_TIMEOUT_MS = 60_000;
|
||||
|
||||
@@ -17,7 +17,7 @@ export function buildRecordingScriptPathCandidates(
|
||||
projectRoot: string,
|
||||
cwd: string,
|
||||
): string[] {
|
||||
const sourceScriptPath = `apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`;
|
||||
const sourceScriptPath = `apple/runner/AgentDeviceRunner/RecordingScripts/${scriptName}`;
|
||||
const packagedScriptPath = `dist/${sourceScriptPath}`;
|
||||
return [
|
||||
path.resolve(moduleDir, scriptName),
|
||||
@@ -46,7 +46,7 @@ function resolveRecordingScriptPath(scriptName: string): string {
|
||||
}
|
||||
|
||||
throw new AppError('COMMAND_FAILED', `Missing recording helper script: ${scriptName}`, {
|
||||
hint: 'Ensure apple-runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled under dist/apple-runner in the package.',
|
||||
hint: 'Ensure apple/runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled under dist/apple/runner in the package.',
|
||||
scriptName,
|
||||
searchedPaths: scriptCandidates,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isTapPointInsideViewport } from '../mobile-snapshot-semantics.ts';
|
||||
import type { Rect } from '../../kernel/snapshot.ts';
|
||||
|
||||
// ADR 0011 Layer 2 golden parity table: the SAME JSON is asserted against the
|
||||
// Swift twin (TapPointPolicy in apple-runner/AgentDeviceRunner/
|
||||
// Swift twin (TapPointPolicy in apple/runner/AgentDeviceRunner/
|
||||
// AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift, gated XCTest in the
|
||||
// same file), so drift between the runner's ELEMENT_OFFSCREEN guard and the
|
||||
// runtime's offscreen rule turns CI red on whichever side changed.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user