Files
software-mansion__argent/packages/tool-server/test/bundle-id-pattern.test.ts
Ignacy Łątka a3312cab97 fix(tool-server): admit a digit-leading bundleId in the four app-lifecycle schemas (#1101)
Fixes #1024

**Cause.** `launch-app`, `restart-app`, `reinstall-app`,
`settings-permissions` and `open-url` each carried their own copy of
`/^[A-Za-z_][A-Za-z0-9._-]*$/`, which rejects an iOS bundle id such as
`9gag.app` - with a message that says digits are allowed, so the agent
reading it has nothing to act on.

**Fix.** One shared `BUNDLE_ID_PATTERN` / `BUNDLE_ID_MESSAGE` in
`src/utils/bundle-id.ts`. The head class becomes `[A-Za-z0-9_]` (the
same class `native-devtools`' handshake validator already uses); `-` and
`.` stay out, which is the whole of the flag-injection guard the old
comments cited. The message names the head rule it enforces.

**Class check.** No copy of the pattern remains (`grep
BUNDLE_ID_PATTERN`). `open-url` was a fifth copy that landed on main
while this branch was open - the catalogue-wide test caught it.
`ACTIVITY_PATTERN` in `launch-app` / `restart-app` keeps its letter-only
head deliberately: an activity is a Java class name and cannot start
with a digit.

**Docs.** No docs change needed - `reference/tools.mdx` lists these
tools by one-line summary and documents no parameter constraints.

<details>
<summary>Verification</summary>

New `test/bundle-id-pattern.test.ts` walks the whole catalogue rather
than one tool, so a sixth `bundleId` constraint has to satisfy the same
rules. Discrimination proved by restoring the old pattern and message in
the shared module and re-running:

```
 × derives a sane MCP JSON schema with bundleId required for every action
 × launch-app: admits a digit-leading bundle id and still refuses a flag
 × launch-app: names the head rule when it rejects one
 × restart-app / reinstall-app / settings-permissions / open-url: both, each
 Tests  11 failed | 61 passed (72)
```

With the fix in place, from the worktree root:

```
npx tsc --build                                  OK
npm run typecheck:tests -w @argent/tool-server   OK
npm test -w @argent/tool-server                  434 files, 6174 passed | 1 skipped
npx prettier --write <changed files>             unchanged
npx knip                                         bundle-id.ts absent from the report
```

`npx eslint` could not run in this checkout
(`packages/docs/tsconfig.json` cannot resolve `@docusaurus/tsconfig`
without a separate `npm ci` in that workspace, a pre-existing
local-environment issue); the ESLint CI job passes.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:12:38 +02:00

50 lines
2.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { ZodObject, ZodTypeAny } from "zod";
import { createRegistry } from "../src/utils/setup-registry";
import { advertisedSchema, definitionsById } from "./helpers/catalog";
// Asserted over the whole catalog rather than per tool, so a fifth bundleId
// constraint has to satisfy the same rules instead of drifting (issue #1024).
describe("advertised bundleId pattern", () => {
const constrained = [...definitionsById(createRegistry())]
.map(([id, definition]) => {
const properties = advertisedSchema(definition)?.properties as
| Record<string, { pattern?: string } | undefined>
| undefined;
const shape = (definition.zodSchema as ZodObject<Record<string, ZodTypeAny>> | undefined)
?.shape;
return { id, pattern: properties?.bundleId?.pattern, field: shape?.bundleId };
})
.filter((entry) => typeof entry.pattern === "string");
it("is advertised by exactly the tools that take an app identifier", () => {
expect(constrained.map(({ id }) => id).sort()).toEqual([
"launch-app",
"open-url",
"reinstall-app",
"restart-app",
"settings-permissions",
]);
});
for (const { id, pattern, field } of constrained) {
it(`${id}: admits a digit-leading bundle id and still refuses a flag`, () => {
const re = new RegExp(pattern!);
// An iOS CFBundleIdentifier may begin with a digit; `9gag.app` ships.
expect(re.test("9gag.app")).toBe(true);
expect(re.test("3d.tools.app")).toBe(true);
expect(re.test("com.example.app")).toBe(true);
// The head keeps out anything argv could read as a flag or a dotfile.
expect(re.test("--user")).toBe(false);
expect(re.test("-user")).toBe(false);
expect(re.test(".hidden")).toBe(false);
});
it(`${id}: names the head rule when it rejects one`, () => {
const rejected = field!.safeParse("--user");
expect(rejected.success).toBe(false);
expect(rejected.error!.issues[0]!.message).toContain("may not start with '-' or '.'");
});
}
});