Files
software-mansion__argent/packages/tool-server/test/native-target-app.test.ts
Ignacy Łątka 43d5b9d464 fix(native-devtools): let only a verdict that was reached be repeated
`indeterminate` turned terminal on `wasAdvisedToRelaunch` alone, which is a pid
change since the hand-out and nothing more — already true one `connecting`
reading after the relaunch. One unreadable `ps` in that window answered
`injection_failed` for an app two seconds into a healthy cold start, asserting an
earlier reading that measured a fresh process which never connected when the
earlier reading had said "wait a few seconds".

The verdict is now recorded when it is handed out, and `indeterminate` repeats
that record rather than inferring one. Both terminal readings share the endpoint
withdrawal too: losing the socket makes the premise a statement about the socket
for the unreadable reading exactly as for the measured one.

describe stops pairing `should_restart: true` — rendered to an agent as "call
restart-app and retry" — with a message that says no restart on either side
changes anything, and the six feature tools' `service_stale` clause gains the
caveat its `native-devtools-status` twin already carries, now that the status can
arrive with the endpoint-lost message.
2026-09-08 11:52:07 +02:00

164 lines
5.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
chooseFrontmostConnectedApp,
inspectConnectedNativeApps,
resolveNativeTargetApp,
} from "../src/utils/native-target-app";
import type { NativeAppState, NativeDevtoolsApi } from "../src/blueprints/native-devtools";
function makeAppState(bundleId: string, overrides: Partial<NativeAppState> = {}): NativeAppState {
return {
bundleId,
applicationState: "background",
foregroundActiveSceneCount: 0,
foregroundInactiveSceneCount: 0,
backgroundSceneCount: 1,
unattachedSceneCount: 0,
isFrontmostCandidate: false,
...overrides,
};
}
function makeApi(apps: NativeAppState[]): NativeDevtoolsApi {
const byBundleId = new Map(apps.map((app) => [app.bundleId, app]));
const relaunchAdvised = new Set<string>();
const terminalVerdict = new Set<string>();
return {
isEnvSetup: () => true,
socketPath: "/tmp/mock.sock",
ensureEnvReady: async () => {},
reverifyEnv: async () => {},
armsEnv: true,
withdrawEnv: async () => {},
getInitFailure: () => null,
isConnected: (bundleId) => byBundleId.has(bundleId),
isAppRunning: async (bundleId) => byBundleId.has(bundleId),
listConnectedBundleIds: () => [...byBundleId.keys()],
holdsEndpoint: () => true,
noteRelaunchAdvice: (bundleId: string) => {
relaunchAdvised.add(bundleId);
},
wasAdvisedToRelaunch: (bundleId: string) => relaunchAdvised.has(bundleId),
noteTerminalVerdict: (bundleId: string) => {
terminalVerdict.add(bundleId);
},
verdictStands: (bundleId: string) => terminalVerdict.has(bundleId),
appConnectionState: async () => "connected",
activateNetworkInspection: () => {},
getNetworkLog: () => [],
clearNetworkLog: () => {},
getAppState: async (bundleId) => {
const app = byBundleId.get(bundleId);
if (!app) throw new Error(`unknown bundleId: ${bundleId}`);
return app;
},
detectFrontmostBundleId: async () => null,
queryViewHierarchy: async () => ({}),
};
}
describe("native-target-app", () => {
describe("chooseFrontmostConnectedApp", () => {
it("returns the unique strong candidate when one app is active", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "background" }),
makeAppState("com.example.b", { applicationState: "active" }),
]);
expect(result?.bundleId).toBe("com.example.b");
});
it("returns the unique weak candidate when no strong candidate exists", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "background" }),
makeAppState("com.example.b", {
applicationState: "inactive",
foregroundInactiveSceneCount: 1,
}),
]);
expect(result?.bundleId).toBe("com.example.b");
});
it("returns null when multiple strong candidates exist", () => {
const result = chooseFrontmostConnectedApp([
makeAppState("com.example.a", { applicationState: "active" }),
makeAppState("com.example.b", { foregroundActiveSceneCount: 1 }),
]);
expect(result).toBeNull();
});
});
describe("inspectConnectedNativeApps", () => {
it("returns connected apps sorted by bundle id", async () => {
const api = makeApi([makeAppState("com.example.z"), makeAppState("com.example.a")]);
const result = await inspectConnectedNativeApps(api);
expect(result.map((app) => app.bundleId)).toEqual(["com.example.a", "com.example.z"]);
});
});
describe("resolveNativeTargetApp", () => {
it("uses explicit bundleId when provided", async () => {
const api = makeApi([makeAppState("com.example.a")]);
await expect(resolveNativeTargetApp(api, "com.example.explicit")).resolves.toEqual({
bundleId: "com.example.explicit",
source: "explicit",
});
});
it("auto-selects the only connected app", async () => {
const api = makeApi([
makeAppState("com.example.only", {
applicationState: "active",
foregroundActiveSceneCount: 1,
}),
]);
await expect(resolveNativeTargetApp(api)).resolves.toEqual({
bundleId: "com.example.only",
source: "single_connected_foreground_like",
});
});
it("requires explicit bundleId when the only connected app is background-only", async () => {
const api = makeApi([makeAppState("com.example.background-only")]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow(
"Provide bundleId explicitly if you still want to target this app."
);
});
it("auto-selects the unique frontmost connected app", async () => {
const api = makeApi([
makeAppState("com.example.background"),
makeAppState("com.example.front", { applicationState: "active" }),
]);
await expect(resolveNativeTargetApp(api)).resolves.toEqual({
bundleId: "com.example.front",
source: "frontmost_detected",
});
});
it("throws when no connected apps are available", async () => {
const api = makeApi([]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow(
"No native-devtools-connected apps are available for auto-targeting. Launch or restart the app first, provide bundleId explicitly, or use screenshot to inspect visible Home/system UI."
);
});
it("throws when multiple connected apps exist and none is uniquely frontmost", async () => {
const api = makeApi([
makeAppState("com.example.a", { applicationState: "active" }),
makeAppState("com.example.b", { foregroundActiveSceneCount: 1 }),
]);
await expect(resolveNativeTargetApp(api)).rejects.toThrow("Provide bundleId explicitly.");
});
});
});