mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
0b3a407cc3
Three gaps the review's mutation sweep and reading found, none of which any existing assertion covered: - `http.ts`'s `if (def.longRunning) entry.longRunning = true` was untested. That one line is what the whole no-timeout mechanism rides on — including, positively, stopping the MCP adapter re-sending a timed-out, non-idempotent `keyboard` POST up to five times. - the during-typing split's error code survived being swapped for INEFFECTIVE. The two have to stay distinguishable on the wire: one means "re-clear required", the other "the clear worked, the characters went elsewhere", and `failure_stage` is not serialized. - the helper gate was only ever driven at RUNNING and IDLE, though `isLiveServiceState` also admits STARTING — a helper mid-start holds the UiAutomation connection just as a running one does.
557 lines
21 KiB
TypeScript
557 lines
21 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import supertest from "supertest";
|
|
import { z } from "zod";
|
|
import { createHttpApp, type HttpAppHandle } from "../src/http";
|
|
import type { Registry } from "@argent/registry";
|
|
|
|
vi.mock("../src/utils/update-checker", () => ({
|
|
getUpdateState: vi.fn(() => ({
|
|
updateAvailable: false,
|
|
latestVersion: null,
|
|
currentVersion: "1.0.0",
|
|
})),
|
|
isUpdateNoteSuppressed: vi.fn(() => true),
|
|
suppressUpdateNote: vi.fn(),
|
|
}));
|
|
|
|
// Drive the synchronous, cache-only runtime-kind readers that http.ts consults
|
|
// to split a TV target out of its base mobile platform. Real code warms these
|
|
// caches via simctl/adb; here we mock only the two getters so a case can pretend
|
|
// the cache is cold ("mobile"/undefined → coarse platform) or warm for TV.
|
|
const tvKinds = vi.hoisted(() => ({
|
|
ios: undefined as "mobile" | "tv" | undefined,
|
|
android: undefined as "mobile" | "tv" | undefined,
|
|
}));
|
|
|
|
vi.mock("../src/utils/ios-devices", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../src/utils/ios-devices")>();
|
|
return { ...actual, getCachedSimulatorRuntimeKind: () => tvKinds.ios };
|
|
});
|
|
|
|
vi.mock("../src/utils/adb", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../src/utils/adb")>();
|
|
return { ...actual, getCachedAndroidRuntimeKind: () => tvKinds.android };
|
|
});
|
|
|
|
function stubRegistry(): Registry {
|
|
return {
|
|
getSnapshot: vi.fn(() => ({
|
|
services: new Map(),
|
|
namespaces: [],
|
|
tools: ["always-tool", "hinted-tool", "plain-tool", "device-tool", "boot-tool"],
|
|
})),
|
|
getTool: vi.fn((name: string) => {
|
|
if (name === "always-tool") {
|
|
return {
|
|
id: "always-tool",
|
|
description: "Always loaded",
|
|
inputSchema: { type: "object", properties: {} },
|
|
alwaysLoad: true,
|
|
searchHint: "always loaded discovery",
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
};
|
|
}
|
|
if (name === "hinted-tool") {
|
|
return {
|
|
id: "hinted-tool",
|
|
description: "Deferred but hinted",
|
|
inputSchema: { type: "object", properties: {} },
|
|
searchHint: "profiling hotspots cpu",
|
|
// Declared here as well, because this flag is what the MCP adapter
|
|
// reads to decide NOT to apply its 30s fetch timeout — and not to
|
|
// re-send a timed-out, non-idempotent POST up to five times.
|
|
longRunning: true,
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
};
|
|
}
|
|
if (name === "plain-tool") {
|
|
return {
|
|
id: "plain-tool",
|
|
description: "Plain tool",
|
|
inputSchema: { type: "object", properties: {} },
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
};
|
|
}
|
|
if (name === "device-tool") {
|
|
return {
|
|
id: "device-tool",
|
|
description: "Device tool",
|
|
inputSchema: { type: "object", properties: {} },
|
|
capability: { apple: { simulator: true }, android: { emulator: true } },
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
};
|
|
}
|
|
if (name === "boot-tool") {
|
|
return {
|
|
id: "boot-tool",
|
|
description: "Boot tool",
|
|
inputSchema: { type: "object", properties: {} },
|
|
capability: { apple: { simulator: true }, android: { emulator: true } },
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
};
|
|
}
|
|
return undefined;
|
|
}),
|
|
invokeTool: vi.fn(),
|
|
} as unknown as Registry;
|
|
}
|
|
|
|
describe("GET /tools progressive-loading metadata", () => {
|
|
let handle: HttpAppHandle;
|
|
const request = supertest;
|
|
|
|
beforeEach(async () => {
|
|
tvKinds.ios = undefined;
|
|
tvKinds.android = undefined;
|
|
handle = createHttpApp(stubRegistry());
|
|
});
|
|
|
|
afterEach(() => {
|
|
handle?.dispose();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("passes alwaysLoad and searchHint through on /tools response", async () => {
|
|
const res = await request(handle.app).get("/tools").expect(200);
|
|
const byName = new Map<string, Record<string, unknown>>(
|
|
(res.body.tools as Record<string, unknown>[]).map((t) => [t.name as string, t])
|
|
);
|
|
|
|
expect(byName.get("always-tool")).toMatchObject({
|
|
alwaysLoad: true,
|
|
searchHint: "always loaded discovery",
|
|
});
|
|
expect(byName.get("hinted-tool")).toMatchObject({
|
|
searchHint: "profiling hotspots cpu",
|
|
longRunning: true,
|
|
});
|
|
expect(byName.get("hinted-tool")).not.toHaveProperty("alwaysLoad");
|
|
expect(byName.get("plain-tool")).not.toHaveProperty("alwaysLoad");
|
|
expect(byName.get("plain-tool")).not.toHaveProperty("searchHint");
|
|
// Absent, not `false`: the adapter keys on the property being there, and
|
|
// `keyboard`'s whole no-timeout mechanism rides on this one line.
|
|
expect(byName.get("plain-tool")).not.toHaveProperty("longRunning");
|
|
});
|
|
|
|
it("does not pass bundleId into telemetry invocation metadata", async () => {
|
|
const release = vi.fn();
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_toolInvocationId: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return release;
|
|
});
|
|
const registry = stubRegistry();
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({
|
|
udid: "11111111-1111-1111-1111-111111111111",
|
|
bundleId: "com.example.app",
|
|
})
|
|
.expect(200);
|
|
|
|
expect(recordInvocation).toHaveBeenCalledWith(expect.any(String), {
|
|
platform: "ios",
|
|
});
|
|
expect(seenMeta).not.toHaveProperty("bundleId");
|
|
expect(seenMeta).not.toHaveProperty("deviceId");
|
|
expect(release).toHaveBeenCalledOnce();
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"device-tool",
|
|
expect.any(Object),
|
|
expect.objectContaining({ toolInvocationId: recordInvocation.mock.calls[0]![0] })
|
|
);
|
|
});
|
|
|
|
it("does not record platform metadata for non-device tools", async () => {
|
|
const recordInvocation = vi.fn(() => vi.fn());
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/plain-tool")
|
|
.send({
|
|
udid: "11111111-1111-1111-1111-111111111111",
|
|
bundleId: "com.example.app",
|
|
})
|
|
.expect(200);
|
|
|
|
expect(recordInvocation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("records Android platform for avdName device-management calls without a device hash", async () => {
|
|
const recordInvocation = vi.fn((_toolInvocationId: string, meta: Record<string, unknown>) => {
|
|
expect(meta).toEqual({ platform: "android" });
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app).post("/tools/boot-tool").send({ avdName: "Pixel_9" }).expect(200);
|
|
|
|
expect(recordInvocation).toHaveBeenCalledWith(expect.any(String), { platform: "android" });
|
|
});
|
|
|
|
it("records the platform of a scoped teardown, whose device arg is a LIST", async () => {
|
|
// `devices` is the third device-arg spelling and the only one that is an
|
|
// array — `stop-all-simulator-servers`' scope. The other two spellings are
|
|
// pinned above; deleting the `devices` branch of `extractDeviceArg` left
|
|
// the whole suite green, so a scoped teardown silently lost its platform.
|
|
// Driven through `device-tool` because `extractInvocationMeta` derives a
|
|
// platform only for a tool that declares a capability, and
|
|
// `stop-all-simulator-servers` — the sole tool spelling `devices` today —
|
|
// declares none. That is a fact about THIS consumer: `extractDeviceArg`'s
|
|
// other two are ungated and read `devices` in production (a failure
|
|
// classified from `req.body`, and a replayed teardown step attributed
|
|
// through `deriveChildInvocationMeta`).
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ devices: ["emulator-5554", "11111111-1111-1111-1111-111111111111"] })
|
|
.expect(200);
|
|
|
|
// The first id is enough for the coarse platform; a mixed-platform scope
|
|
// is not something this dimension tries to represent.
|
|
expect(seenMeta).toEqual({ platform: "android" });
|
|
});
|
|
|
|
it("ignores a devices list that holds no usable id", async () => {
|
|
// `devices: []` and `devices: [123]` must both yield no device arg. The
|
|
// empty case alone was a tautology — deleting the
|
|
// `typeof record.devices[0] === "string"` guard left the whole suite green,
|
|
// because a non-string element was never sent. The schema rejects such a
|
|
// call in production, so this is the guard's only exercise.
|
|
const recordInvocation = vi.fn(() => vi.fn());
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
for (const devices of [[], [123], [null]]) {
|
|
recordInvocation.mockClear();
|
|
await request(handle.app).post("/tools/device-tool").send({ devices }).expect(200);
|
|
// No device arg, so no platform — and with nothing else to record, no
|
|
// invocation metadata at all.
|
|
expect(recordInvocation, `devices: ${JSON.stringify(devices)}`).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it("classifies a FAILED call from its devices scope, with no capability in play", async () => {
|
|
// `emitHttpFailure` is one of `extractDeviceArg`'s two UNGATED consumers,
|
|
// and the one that makes the `devices` branch live in production: a
|
|
// rejected `stop-all-simulator-servers` call is classified straight from
|
|
// `req.body`, which carries the scope. Driven through a tool that declares
|
|
// no capability, exactly like the real one.
|
|
const recordFailure = vi.fn();
|
|
const registry = stubRegistry();
|
|
// The real shape: `stop-all-simulator-servers` is `.strict()` precisely so
|
|
// the `udids` slip cannot be stripped down to a machine-wide sweep, and
|
|
// that rejection is a 400 classified from `req.body` — which carries
|
|
// `devices`. No capability anywhere in the path.
|
|
(registry.getTool as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
id: "strict-teardown",
|
|
description: "Scoped teardown",
|
|
inputSchema: { type: "object", properties: { devices: {} } },
|
|
zodSchema: z.object({ devices: z.array(z.string()).optional() }).strict(),
|
|
services: () => ({}),
|
|
execute: async () => ({}),
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordFailure });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/strict-teardown")
|
|
.send({ devices: ["emulator-5554"], udids: ["oops"] })
|
|
.expect(400);
|
|
|
|
expect(recordFailure).toHaveBeenCalled();
|
|
expect(recordFailure.mock.calls[0][1]).toMatchObject({ platform: "android" });
|
|
});
|
|
|
|
it("refines an iOS device to `tvos` when its cached runtime kind is tv", async () => {
|
|
tvKinds.ios = "tv";
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
// Same UDID shape as an iPhone sim, but the warm cache splits it out as tvOS.
|
|
expect(seenMeta).toEqual({ platform: "tvos" });
|
|
});
|
|
|
|
it("refines an Android device to `android-tv` when its cached runtime kind is tv", async () => {
|
|
tvKinds.android = "tv";
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ udid: "emulator-5554" })
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ platform: "android-tv" });
|
|
});
|
|
|
|
it("keeps the coarse `ios` platform when the cached kind is mobile (not tv)", async () => {
|
|
tvKinds.ios = "mobile";
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ platform: "ios" });
|
|
});
|
|
|
|
it("keeps the coarse platform when the cache is cold (first call before warm-up)", async () => {
|
|
// tvKinds default to undefined → the reader can't yet tell TV from mobile, so
|
|
// the first tool call on a device reports the base platform. A later call,
|
|
// once describe/streaming has warmed the cache, would report the TV variant.
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ platform: "ios" });
|
|
});
|
|
|
|
it("re-derives a child sub-tool's TV platform from its own device arg", async () => {
|
|
tvKinds.android = "tv";
|
|
const recordInvocation = vi.fn((_id: string, _meta: Record<string, unknown>) => vi.fn());
|
|
const registry = stubRegistry();
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordInvocation });
|
|
|
|
// Parent targets an Android TV device.
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.send({ udid: "emulator-5554" })
|
|
.expect(200);
|
|
|
|
const { recordChildInvocation } = vi.mocked(registry.invokeTool).mock.calls[0]![2] as {
|
|
recordChildInvocation: (id: string, childArgs?: unknown) => () => void;
|
|
};
|
|
|
|
// A sub-tool targeting the same TV device is attributed to android-tv too.
|
|
recordChildInvocation("tv-child", { udid: "emulator-5554" });
|
|
expect(recordInvocation).toHaveBeenCalledWith("tv-child", { platform: "android-tv" });
|
|
});
|
|
|
|
it("records the AI client from request headers alongside platform", async () => {
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.set("X-Argent-AI-Client", "codex")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ platform: "ios", ai_client: "codex" });
|
|
});
|
|
|
|
it("forwards a child-invocation recorder bound to the request's attribution", async () => {
|
|
const release = vi.fn();
|
|
const recordInvocation = vi.fn((_id: string, _meta: Record<string, unknown>) => release);
|
|
const registry = stubRegistry();
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordInvocation });
|
|
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.set("X-Argent-AI-Client", "codex")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
// The parent invocation is recorded with the resolved attribution.
|
|
expect(recordInvocation).toHaveBeenCalledTimes(1);
|
|
expect(recordInvocation.mock.calls[0]![1]).toEqual({ platform: "ios", ai_client: "codex" });
|
|
|
|
// A recorder is threaded into the tool context so orchestrator tools can
|
|
// attribute the sub-tools they dispatch. The AI client is inherited; a child
|
|
// with no device arg of its own falls back to the request's platform.
|
|
const opts = vi.mocked(registry.invokeTool).mock.calls[0]![2] as {
|
|
recordChildInvocation?: (id: string, childArgs?: unknown) => () => void;
|
|
};
|
|
expect(opts.recordChildInvocation).toBeTypeOf("function");
|
|
|
|
const childRelease = opts.recordChildInvocation!("child-id");
|
|
expect(recordInvocation).toHaveBeenCalledWith("child-id", {
|
|
platform: "ios",
|
|
ai_client: "codex",
|
|
});
|
|
expect(childRelease).toBe(release);
|
|
});
|
|
|
|
it("re-derives each child's platform from its own device arg", async () => {
|
|
const recordInvocation = vi.fn((_id: string, _meta: Record<string, unknown>) => vi.fn());
|
|
const registry = stubRegistry();
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordInvocation });
|
|
|
|
// Parent request targets iOS.
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.set("X-Argent-AI-Client", "codex")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
const { recordChildInvocation } = vi.mocked(registry.invokeTool).mock.calls[0]![2] as {
|
|
recordChildInvocation: (id: string, childArgs?: unknown) => () => void;
|
|
};
|
|
|
|
// A sub-tool that targets an Android device keeps the inherited ai_client but
|
|
// is attributed to ITS OWN platform — not the parent's iOS. This is what lets
|
|
// a flow-execute (no platform of its own) attribute each gesture correctly.
|
|
recordChildInvocation("android-child", { udid: "emulator-5554" });
|
|
expect(recordInvocation).toHaveBeenCalledWith("android-child", {
|
|
ai_client: "codex",
|
|
platform: "android",
|
|
});
|
|
|
|
// A child with no device arg falls back to the parent's platform.
|
|
recordChildInvocation("no-device-child", { message: "hi" });
|
|
expect(recordInvocation).toHaveBeenCalledWith("no-device-child", {
|
|
ai_client: "codex",
|
|
platform: "ios",
|
|
});
|
|
});
|
|
|
|
it("does not forward a child recorder when there is no attribution to propagate", async () => {
|
|
const recordInvocation = vi.fn(() => vi.fn());
|
|
const registry = stubRegistry();
|
|
handle.dispose();
|
|
handle = createHttpApp(registry, { recordInvocation });
|
|
|
|
// plain-tool has no capability and no AI-client header → no metadata at all,
|
|
// so nothing is recorded and no child recorder is handed downstream.
|
|
await request(handle.app).post("/tools/plain-tool").send({}).expect(200);
|
|
|
|
expect(recordInvocation).not.toHaveBeenCalled();
|
|
const opts = vi.mocked(registry.invokeTool).mock.calls[0]![2] as {
|
|
recordChildInvocation?: unknown;
|
|
};
|
|
expect(opts.recordChildInvocation).toBeUndefined();
|
|
});
|
|
|
|
it("records the coarse `other` bucket for an unknown tool, never its name", async () => {
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
// A non-device tool with no platform context still records because the
|
|
// coarse `other` slug is present. The name header is ignored entirely.
|
|
await request(handle.app)
|
|
.post("/tools/plain-tool")
|
|
.set("X-Argent-AI-Client", "other")
|
|
.set("X-Argent-AI-Client-Name", "some-new-tool")
|
|
.send({})
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ ai_client: "other" });
|
|
});
|
|
|
|
it("drops unregistered AI client slugs", async () => {
|
|
const recordInvocation = vi.fn(() => vi.fn());
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
// Unknown slug + unsafe name → nothing usable → no metadata → not recorded
|
|
// for a non-device tool.
|
|
await request(handle.app)
|
|
.post("/tools/plain-tool")
|
|
.set("X-Argent-AI-Client", "evil-client")
|
|
.set("X-Argent-AI-Client-Name", "/Users/alice/secret")
|
|
.send({})
|
|
.expect(200);
|
|
|
|
expect(recordInvocation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("never records a client-name header, even for a recognized client", async () => {
|
|
let seenMeta: Record<string, unknown> | undefined;
|
|
const recordInvocation = vi.fn((_id: string, meta: Record<string, unknown>) => {
|
|
seenMeta = meta;
|
|
return vi.fn();
|
|
});
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
// The raw client name is never recorded; only the coarse slug (and platform)
|
|
// survive, regardless of what name header the MCP server forwarded.
|
|
await request(handle.app)
|
|
.post("/tools/device-tool")
|
|
.set("X-Argent-AI-Client", "codex")
|
|
.set("X-Argent-AI-Client-Name", "claude-code")
|
|
.send({ udid: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(200);
|
|
|
|
expect(seenMeta).toEqual({ platform: "ios", ai_client: "codex" });
|
|
});
|
|
|
|
it("drops a client name sent with no ai_client header", async () => {
|
|
const recordInvocation = vi.fn(() => vi.fn());
|
|
handle.dispose();
|
|
handle = createHttpApp(stubRegistry(), { recordInvocation });
|
|
|
|
// Name-only, no slug → nothing usable → no metadata → not recorded for a
|
|
// non-device tool.
|
|
await request(handle.app)
|
|
.post("/tools/plain-tool")
|
|
.set("X-Argent-AI-Client-Name", "some-new-tool")
|
|
.send({})
|
|
.expect(200);
|
|
|
|
expect(recordInvocation).not.toHaveBeenCalled();
|
|
});
|
|
});
|