Files
software-mansion__argent/packages/tool-server/test/workspace-reader-integration.test.ts
Ignacy Łątka d017ba340c feat: Android emulator support (#148)
### For reviewers - what to focus on:

The diff is ~6k LoC; this breakdown should help triage where to spend
attention:

| Bucket | Lines | Effort |
| --- | --- | --- |
| **Net-new Android logic**: `boot-device.ts` (660) + `adb.ts` (399) +
`uiautomator-parser.ts` (292) | 1351 | **Read carefully** |
| Blueprint `DeviceInfo` refactor: `simulator-server`, `ax-service`,
`native-devtools` | 365 | Read |
| Tool `index.ts` / `types.ts` sprawl across ~20 tools (capability
gates, `services()`, descriptions) | ~965 | Skim — watch for cross-file
consistency |
| `platforms/android.ts` impls (stub → real implementation) | ~426 |
Skim |
| Skill/docs prose, profiler rename ripple | ~1000 | Skim |
| **`platforms/ios.ts` deletions** — 12 files, 0 additions; logic moved
into unified handlers | 684 | **Skip — mechanical** |

## What works, what doesn't:

### Just works thanks to either sim server or metro:
- `gesture-*`
- `button`, `keyboard`
- `screenshot`
- `run-sequence`, `flow-*`
- `debugger-*`
- `react-profiler-*`

### Adjusted to work on android too:

| tool | android | ios |
| --- | --- | --- |
| `describe` | `uiautomator dump` → `DescribeNode` | `AXServiceApi` |
| `launch-app` | `cmd package resolve-activity` + `am start -W` |
`simctl launch` |
| `restart-app` | `cmd package resolve-activity` + `am force-stop` + `am
start -W` | `simctl terminate` + `simctl launch` |
| `reinstall-app` | `adb install -r` (+`-g`/`-d`) | `simctl uninstall` +
`simctl install` |
| `open-url` | `am start -a VIEW -d <url>` | `simctl openurl` |
| `list-devices` | `adb devices` + `emulator -list-avds` | `xcrun simctl
list --json` |
| `boot-device` | snapshot probe → `-force-snapshot-load` hot boot with
screencap-alive guard, falls back to cold boot on any failure | `simctl
boot` + `bootstatus -b` |

### Will not work on android for now:
- `native-devtools-*` (7 total)
- `native-profiler-*` (3 total — was `ios-profiler-*`)
- `profiler-stack-query`, `profiler-combined-report` (depend on
`native-profiler-*`)
- `paste` — unregistered on both platforms for now.

---------

Co-authored-by: Paweł Fornagiel <pawel.fornagiel2@gmail.com>
2026-05-11 11:39:10 +02:00

305 lines
11 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { readWorkspaceSnapshot } from "../src/utils/workspace-reader";
/**
* Integration test: builds a realistic React Native project directory tree and
* validates the full readWorkspaceSnapshot output end-to-end.
*/
let projectDir: string;
async function writeJson(dir: string, name: string, data: unknown) {
await writeFile(join(dir, name), JSON.stringify(data, null, 2));
}
async function writeText(path: string, content: string) {
await writeFile(path, content);
}
beforeAll(async () => {
projectDir = await mkdtemp(join(tmpdir(), "rn-project-integration-"));
// ── package.json ───────────────────────────────────────────────
await writeJson(projectDir, "package.json", {
"name": "IntegrationTestApp",
"version": "2.1.0",
"scripts": {
"start": "react-native start",
"start:local": "LOCAL_API=true react-native start",
"ios": "react-native run-ios",
"android": "react-native run-android",
"test": "jest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"tsc": "tsc --noEmit",
"format": "prettier --write .",
},
"dependencies": {
"react": "18.2.0",
"react-native": "0.74.2",
"react-native-reanimated": "^3.8.0",
"@react-navigation/native": "^6.1.9",
"zustand": "^4.5.0",
"react-native-config": "^1.5.0",
},
"devDependencies": {
"typescript": "^5.4.0",
"jest": "^29.7.0",
"eslint": "^8.57.0",
"prettier": "^3.2.0",
"lint-staged": { "*.{ts,tsx}": ["eslint --fix", "prettier --write"] },
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
},
});
// ── Metro config with custom port ──────────────────────────────
await writeText(
join(projectDir, "metro.config.js"),
`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const config = {
server: {
port: 8082,
},
transformer: {
getTransformOptions: async () => ({
transform: { experimentalImportSupport: false, inlineRequires: true },
}),
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
`
);
// ── Babel config ───────────────────────────────────────────────
await writeText(
join(projectDir, "babel.config.js"),
`module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-reanimated/plugin'],
};
`
);
// ── tsconfig.json ──────────────────────────────────────────────
await writeJson(projectDir, "tsconfig.json", {
compilerOptions: {
strict: true,
target: "esnext",
module: "commonjs",
jsx: "react-native",
moduleResolution: "node",
},
include: ["src"],
});
// ── app.json ───────────────────────────────────────────────────
await writeJson(projectDir, "app.json", {
name: "IntegrationTestApp",
displayName: "Integration Test App",
});
// ── eas.json ───────────────────────────────────────────────────
await writeJson(projectDir, "eas.json", {
build: {
development: { distribution: "internal", ios: { simulator: true } },
staging: { distribution: "internal" },
production: {},
},
});
// ── eslint config ──────────────────────────────────────────────
await writeJson(projectDir, ".eslintrc.json", {
extends: "@react-native",
rules: { "no-console": "warn" },
});
// ── prettier config ────────────────────────────────────────────
await writeJson(projectDir, ".prettierrc.json", {
singleQuote: true,
trailingComma: "all",
});
// ── jest config ────────────────────────────────────────────────
await writeText(
join(projectDir, "jest.config.js"),
`module.exports = { preset: 'react-native', testPathIgnorePatterns: ['/node_modules/', '/e2e/'] };`
);
// ── iOS directory ──────────────────────────────────────────────
await mkdir(join(projectDir, "ios", "IntegrationTestApp.xcworkspace"), {
recursive: true,
});
await writeText(
join(projectDir, "ios", "Podfile"),
`platform :ios, '14.0'\nuse_frameworks! :linkage => :static\n`
);
// ── Android directory ──────────────────────────────────────────
await mkdir(join(projectDir, "android", "app"), { recursive: true });
await writeText(
join(projectDir, "android", "build.gradle"),
`buildscript { ext { buildToolsVersion = "34.0.0" } }`
);
// ── Yarn lockfile ──────────────────────────────────────────────
await writeText(join(projectDir, "yarn.lock"), "# yarn lockfile v1\n");
// ── .env files ─────────────────────────────────────────────────
await writeText(
join(projectDir, ".env"),
"API_URL=https://api.production.example.com\nANALYTICS_KEY=prod_key_123\n"
);
await writeText(
join(projectDir, ".env.local"),
"API_URL=http://localhost:3000\nDEBUG_MODE=true\n"
);
// ── Makefile ───────────────────────────────────────────────────
await writeText(
join(projectDir, "Makefile"),
`setup:\n\tyarn install\n\tcd ios && pod install\n\nlint:\n\tyarn lint\n\ntest:\n\tyarn test\n\ntypecheck:\n\tyarn tsc\n`
);
// ── scripts/ directory ─────────────────────────────────────────
await mkdir(join(projectDir, "scripts"));
await writeText(join(projectDir, "scripts", "seed-data.sh"), "#!/bin/bash\necho seeding");
await writeText(join(projectDir, "scripts", "migrate.js"), "// migration script");
// ── .husky/ directory ──────────────────────────────────────────
await mkdir(join(projectDir, ".husky"));
await writeText(join(projectDir, ".husky", "pre-commit"), "#!/usr/bin/env sh\nnpx lint-staged");
// ── GitHub Actions CI ──────────────────────────────────────────
await mkdir(join(projectDir, ".github", "workflows"), { recursive: true });
await writeText(
join(projectDir, ".github", "workflows", "ci.yml"),
"name: CI\non: [push]\njobs:\n test:\n runs-on: ubuntu-latest\n"
);
// ── .vscode/launch.json ────────────────────────────────────────
await mkdir(join(projectDir, ".vscode"));
await writeJson(join(projectDir, ".vscode"), "launch.json", {
version: "0.2.0",
configurations: [],
});
});
afterAll(async () => {
await rm(projectDir, { recursive: true, force: true });
});
describe("workspace-reader integration (realistic RN project)", () => {
it("returns all expected fields", async () => {
const snap = await readWorkspaceSnapshot(projectDir);
// Top-level workspace path
expect(snap.workspace_path).toBe(projectDir);
// package.json parsed correctly
expect(snap.package_json).toBeDefined();
expect(snap.package_json!.name).toBe("IntegrationTestApp");
expect(snap.package_json!.version).toBe("2.1.0");
const scripts = snap.package_json!.scripts as Record<string, string>;
expect(scripts["start:local"]).toContain("LOCAL_API");
// Metro config
expect(snap.metro_config_raw).toContain("mergeConfig");
expect(snap.metro_config_raw).toContain("port: 8082");
expect(snap.metro_port).toBe(8082);
// Babel config
expect(snap.babel_config_raw).toContain("react-native-reanimated/plugin");
// app.json
expect(snap.app_json).toMatchObject({
name: "IntegrationTestApp",
displayName: "Integration Test App",
});
// eas.json
expect(snap.eas_json).toBeDefined();
expect(snap.eas_json!.build).toBeDefined();
// tsconfig
expect(snap.tsconfig).toBeDefined();
expect((snap.tsconfig!.compilerOptions as Record<string, unknown>).strict).toBe(true);
// Platform directories
expect(snap.has_ios_dir).toBe(true);
expect(snap.has_android_dir).toBe(true);
expect(snap.ios_workspace).toBe("IntegrationTestApp.xcworkspace");
expect(snap.ios_has_podfile).toBe(true);
// Lockfile
expect(snap.lockfile).toBe("yarn.lock");
// .env files (keys only, no values)
expect(snap.env_files).toHaveLength(2);
const envMain = snap.env_files.find((e) => e.name === ".env")!;
expect(envMain.keys).toContain("API_URL");
expect(envMain.keys).toContain("ANALYTICS_KEY");
const envLocal = snap.env_files.find((e) => e.name === ".env.local")!;
expect(envLocal.keys).toContain("API_URL");
expect(envLocal.keys).toContain("DEBUG_MODE");
// Verify no secrets leaked
const serialized = JSON.stringify(snap.env_files);
expect(serialized).not.toContain("prod_key_123");
expect(serialized).not.toContain("https://api.production.example.com");
expect(serialized).not.toContain("http://localhost:3000");
// Tool versions
expect(snap.tool_versions).toHaveProperty("node");
expect(snap.tool_versions.node).toBeTruthy();
// scripts/ directory
expect(snap.scripts_dir_entries).toContain("seed-data.sh");
expect(snap.scripts_dir_entries).toContain("migrate.js");
// .husky/ hooks
expect(snap.husky_hooks).toContain("pre-commit");
// CI config
expect(snap.ci_config).toBe("github-actions");
// Makefile targets
expect(snap.makefile_targets).toEqual(
expect.arrayContaining(["setup", "lint", "test", "typecheck"])
);
// lint-staged config
expect(snap.lint_staged_config).toEqual({
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
});
// Config files found
expect(snap.config_files_found).toEqual(
expect.arrayContaining([
"metro.config.js",
"babel.config.js",
"tsconfig.json",
"app.json",
"eas.json",
".eslintrc.json",
"jest.config.js",
"Makefile",
".vscode/launch.json",
])
);
});
it("completes in under 5 seconds", async () => {
const start = performance.now();
await readWorkspaceSnapshot(projectDir);
const elapsed = performance.now() - start;
expect(elapsed).toBeLessThan(5_000);
});
});