mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
6e9e45c9f0
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 -->
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
/**
|
|
* The revocation gate at the HTTP edge, in the case where revocation does not
|
|
* work.
|
|
*
|
|
* A provider narrowing or withdrawing a grant only bites because the dispatch
|
|
* edge drops the warm handles first. The capability gate itself runs in the
|
|
* factory and a cached service never re-enters one. So a sweep that cannot
|
|
* finish is not a tidy-up that failed, it is the enforcement point failing and
|
|
* the request behind it has to stop rather than reach a service the provider
|
|
* has taken back.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import request from "supertest";
|
|
import { Registry } from "@argent/registry";
|
|
import { z } from "zod";
|
|
|
|
const enforceMock = vi.fn();
|
|
|
|
vi.mock("../src/utils/external-devices", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../src/utils/external-devices")>();
|
|
|
|
return {
|
|
...actual,
|
|
enforceExternalDeviceGrant: (...args: unknown[]) => enforceMock(...args),
|
|
};
|
|
});
|
|
|
|
import { createHttpApp } from "../src/http";
|
|
|
|
const DEVICE_ID = "ext:acme-3f2a9c:1A2B3C4D-5E6F-7081-92A3-B4C5D6E7F809";
|
|
|
|
/**
|
|
* A tool that reports whether it ran, so "did the request get through" is
|
|
* observable.
|
|
*/
|
|
function registryWithProbe(ran: { value: boolean }): Registry {
|
|
const registry = new Registry();
|
|
|
|
registry.registerTool({
|
|
execute: async () => {
|
|
ran.value = true;
|
|
return { ok: true };
|
|
},
|
|
id: "probe",
|
|
services: () => ({}),
|
|
zodSchema: z.object({ device_id: z.string() }),
|
|
});
|
|
|
|
return registry;
|
|
}
|
|
|
|
describe("a revocation sweep that cannot finish stops the request", () => {
|
|
beforeEach(() => {
|
|
enforceMock.mockReset();
|
|
});
|
|
|
|
it("answers 500 and does not invoke the tool", async () => {
|
|
enforceMock.mockRejectedValue(new Error("SimulatorServer:ext:acme-3f2a9c:… (teardown wedged)"));
|
|
|
|
const ran = { value: false };
|
|
const { app } = createHttpApp(registryWithProbe(ran));
|
|
|
|
const res = await request(app).post("/tools/probe").send({ device_id: DEVICE_ID });
|
|
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toMatch(/teardown wedged/);
|
|
expect(ran.value).toBe(false);
|
|
});
|
|
|
|
it("records the failure under its own signal", async () => {
|
|
enforceMock.mockRejectedValue(new Error("nope"));
|
|
|
|
const recordFailure = vi.fn();
|
|
const { app } = createHttpApp(registryWithProbe({ value: false }), { recordFailure });
|
|
|
|
await request(app).post("/tools/probe").send({ device_id: DEVICE_ID });
|
|
|
|
expect(recordFailure).toHaveBeenCalledWith(
|
|
"probe",
|
|
expect.objectContaining({ device_provider: "acme" }),
|
|
{
|
|
error_code: "EXTERNAL_DEVICE_REVOCATION_INCOMPLETE",
|
|
failure_stage: "external_device_revocation",
|
|
failure_area: "http",
|
|
error_kind: "unknown",
|
|
},
|
|
expect.any(Number)
|
|
);
|
|
});
|
|
|
|
/** The control. A sweep that completes hands the call on as it always did. */
|
|
it("dispatches when the sweep completes", async () => {
|
|
enforceMock.mockResolvedValue({ reason: "its provider changed what it grants", stale: true });
|
|
|
|
const ran = { value: false };
|
|
const { app } = createHttpApp(registryWithProbe(ran));
|
|
|
|
const res = await request(app).post("/tools/probe").send({ device_id: DEVICE_ID });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(ran.value).toBe(true);
|
|
});
|
|
|
|
/** Nothing stale means nothing was dropped, so the gate stays quiet. */
|
|
it("says nothing when the grant is unchanged", async () => {
|
|
enforceMock.mockResolvedValue({ stale: false });
|
|
const written = vi.spyOn(process.stderr, "write").mockReturnValue(true);
|
|
|
|
try {
|
|
const ran = { value: false };
|
|
const { app } = createHttpApp(registryWithProbe(ran));
|
|
|
|
const res = await request(app).post("/tools/probe").send({ device_id: DEVICE_ID });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(ran.value).toBe(true);
|
|
expect(written.mock.calls.flat().join("")).not.toMatch(/dropped cached services/);
|
|
} finally {
|
|
written.mockRestore();
|
|
}
|
|
});
|
|
});
|