Files
software-mansion__argent/packages/tool-server/test/native-devtools-env.test.ts
Mateusz Aliyev 6e9e45c9f0 feat: attach to devices offered by external providers (#735)
This pull request lets Argent drive a simulator or emulator another
process is already running, attaching to its simulator-server rather
than spawning a second one.

A provider writes a JSON descriptor to `~/.argent/providers/` listing
the devices it offers and the mechanisms Argent may use on each. They
appear in `list-devices` with an `ext:` id and work with the existing
tools. The file is re-read on every call, so a withdrawal or a narrowed
grant applies immediately.

`ios.additionalDeviceSets` (#600) already makes such a simulator
reachable by UDID. What changes is ownership. `boot-device` and
`stop-simulator-server` refuse these devices, anything the provider did
not grant is refused with a message naming it and Argent uses only the
endpoints its own simulator-server build serves. A grant binds to the
device rather than to one of its names, so the real udid or serial is
gated exactly like the `ext:` id. Android emulators are covered too and
a device visible to both a provider and `adb`/`simctl` is listed once.

The contract ships as `schemas/device-provider-v1.json`, validated by
`argent providers check`, and is documented in
`docs/reference/device-providers.mdx`. Includes unit tests and an e2e
phase where the harness acts as its own provider.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for discovering and connecting to externally managed
devices.
* Added `argent providers` commands for listing, validating, publishing,
withdrawing, and pruning providers.
* Added provider-aware simulator, debugger, profiler, native tools, and
device listings.
* Added capability controls, endpoint validation, revocation handling,
and provider-specific diagnostics.
  * Added automatic CLI discovery for provider integrations.
* **Bug Fixes**
  * Improved service recovery and paused-runtime error reporting.
  * Improved simulator keyboard and paste command reliability.
* **Documentation**
* Documented provider descriptors, capabilities, lifecycle rules, and
CLI usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-04 22:02:57 +02:00

167 lines
6.2 KiB
TypeScript

import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
buildDyldInsertLibraries,
withdrawDyldInsertLibraries,
} from "../src/blueprints/native-devtools";
describe("buildDyldInsertLibraries", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
function makeTempFile(name: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "native-devtools-env-"));
tempDirs.push(dir);
const filePath = path.join(dir, name);
fs.writeFileSync(filePath, "");
return filePath;
}
it("strips stale bootstrap entries and non-existent paths, keeps files present on disk", () => {
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const unrelated = makeTempFile("libOtherInspector.dylib");
const staleBootstrap = "/tmp/old/location/libArgentInjectionBootstrap.dylib";
const truncatedEntry = "/Users/filip/.nvm/versi";
const result = buildDyldInsertLibraries(
[staleBootstrap, truncatedEntry, unrelated].join(":"),
currentBootstrap
);
// staleBootstrap stripped (argent basename); truncatedEntry stripped (not on disk)
expect(result).toBe([unrelated, currentBootstrap].join(":"));
});
it("preserves valid non-bootstrap loader entries while deduplicating the active bootstrap", () => {
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const unrelated = makeTempFile("libCustomTracing.dylib");
const result = buildDyldInsertLibraries(
[currentBootstrap, "@loader_path/Other.dylib", unrelated].join(":"),
currentBootstrap
);
expect(result).toBe(["@loader_path/Other.dylib", unrelated, currentBootstrap].join(":"));
});
it("strips stale legacy libInjectionBootstrap.dylib paths when merging with the renamed bootstrap", () => {
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const legacyBootstrap = "/tmp/old/libInjectionBootstrap.dylib";
const thirdParty = makeTempFile("libSimCamLoader.dylib");
const result = buildDyldInsertLibraries(
[legacyBootstrap, thirdParty].join(":"),
currentBootstrap
);
expect(result).toBe([thirdParty, currentBootstrap].join(":"));
});
it("is idempotent when called with its own output", () => {
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const thirdParty = makeTempFile("libSimCamLoader.dylib");
const first = buildDyldInsertLibraries(thirdParty, currentBootstrap);
const second = buildDyldInsertLibraries(first, currentBootstrap);
expect(second).toBe(first);
});
it("cleans up truncated entries from simctl getenv 127-byte corruption", () => {
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const thirdParty = makeTempFile("libSimCamLoader.dylib");
// Simulate the truncated entries that accumulate from the simctl getenv bug
const truncated1 = "/Users/mdk/.nvm/versions/node/v24.5.0/lib/nod";
const truncated2 =
"/Users/mdk/.nvm/versions/node/v24.5.0/lib/node_modules/@swmansion/argent/dylibs/l";
const result = buildDyldInsertLibraries(
[truncated1, truncated2, currentBootstrap, thirdParty].join(":"),
currentBootstrap
);
// Both truncated entries stripped (not on disk), bootstrap deduped, thirdParty preserved
expect(result).toBe([thirdParty, currentBootstrap].join(":"));
});
});
describe("withdrawDyldInsertLibraries", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
function makeTempFile(name: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "native-devtools-env-"));
tempDirs.push(dir);
const filePath = path.join(dir, name);
fs.writeFileSync(filePath, "");
return filePath;
}
/**
* The case this exists for; we armed a simulator a device provider went on to
* claim. Its bootstrap has to survive, because it is the injection the
* provider's app is about to load.
*/
it("removes our bootstrap and leaves a provider's in place", () => {
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const providerBootstrap = makeTempFile("libInjectionBootstrap.dylib");
expect(
withdrawDyldInsertLibraries([providerBootstrap, ourBootstrap].join(":"), ourBootstrap)
).toBe(providerBootstrap);
});
/**
* `null`, not `""`; dyld reads an empty entry as a malformed list, so the
* variable has to be unset rather than blanked.
*/
it("asks for the variable to be unset when only our entries were in it", () => {
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const staleBootstrap = "/tmp/old/location/libArgentInjectionBootstrap.dylib";
expect(
withdrawDyldInsertLibraries([staleBootstrap, ourBootstrap].join(":"), ourBootstrap)
).toBe(null);
expect(withdrawDyldInsertLibraries("", ourBootstrap)).toBe(null);
});
it("keeps third-party and loader-path entries we never owned", () => {
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const thirdParty = makeTempFile("libSimCamLoader.dylib");
expect(
withdrawDyldInsertLibraries(
[thirdParty, "@loader_path/Other.dylib", ourBootstrap].join(":"),
ourBootstrap
)
).toBe([thirdParty, "@loader_path/Other.dylib"].join(":"));
});
it("is a no-op on an environment that never carried us", () => {
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const thirdParty = makeTempFile("libSimCamLoader.dylib");
expect(withdrawDyldInsertLibraries(thirdParty, ourBootstrap)).toBe(thirdParty);
});
it("undoes exactly what arming did", () => {
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
const thirdParty = makeTempFile("libSimCamLoader.dylib");
const armed = buildDyldInsertLibraries(thirdParty, ourBootstrap);
expect(withdrawDyldInsertLibraries(armed, ourBootstrap)).toBe(thirdParty);
});
});