mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
c5bab2a83d
## Bug
`argent run flow-add-step` declares its own `args` field — a JSON string
holding the recorded step's tool arguments. But the CLI flag parser
hardcoded `--args` as a whole-payload escape hatch, so `--args` shadowed
the tool's own `args` field. The documented per-flag form
```
argent run flow-add-step --command gesture-tap --args '{"udid":"...","x":0.5,"y":0.5}'
```
silently dropped the step arguments: the `--args` JSON was consumed as
the whole payload and the tool's `args` field stayed `undefined`,
producing a misleading downstream validation error. Only the nested
full-payload form worked.
## Fix
Make `--args` schema-aware in `parseFlags()`:
- When the tool's schema declares a property named `args`
(`schema.properties.args`), `--args <value>` is routed through the
normal per-field handling and coerced by its declared type (a `string`
for flow-add-step, so the raw JSON string passes straight into the
`args` field). Both `--args value` and `--args=value` forms are
supported.
- When the schema does NOT declare `args`, the existing whole-payload
behavior is preserved exactly, including the `--args -` stdin sentinel.
`printToolHelp()` no longer prints the global whole-payload `--args
<json>` / `--args -` lines for tools that declare their own `args`
field, so the help stops contradicting the per-field `--args` shown in
the schema flags.
The change is general: any tool with an `args` field benefits, with no
special-casing by tool name.
## Verification
- `npm run build` (root tsc) — clean.
- `npm test -w @argent/cli` — 172 passed, including 6 new `parseFlags`
cases covering schema-with-`args` (space-separated + inline forms, plus
sibling fields) and schema-without-`args` (whole-payload preserved,
`--args -` sentinel, no-schema fallback).
- Prettier + ESLint (`--max-warnings 0`) clean on the touched files.
Full CLI end-to-end (running `flow-add-step` against a live tool-server
with an active flow recording) was not exercised here; the added unit
tests prove the parser behavior.
Fixes #452
---
### CLI end-to-end verification
Ran the documented per-flag form against a live tool-server with an
active flow recording:
- **Before (global argent 0.13.0):** `argent run flow-add-step --command
gesture-tap --args '{"udid":"...","x":0.5,"y":0.35}'` → `Invalid params
for tool "gesture-tap": udid expected string, received undefined ...`
(args dropped).
- **After (this branch):** same command → `{ "message": "Step added to
\"colltest\" flow", "toolResult": { "tapped": true } }`, and the
recorded YAML contains the step **with** its args (`udid`/`x`/`y`). The
per-flag form now works as documented.
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { run } from "../src/run.js";
|
|
|
|
// Drive `printToolHelp` through the real `run(..., "--help")` entry point with a
|
|
// mocked tools-client, capturing console.log. This locks in the user-visible
|
|
// half of the fix: a tool that declares its own `args` field must NOT advertise
|
|
// the whole-payload `--args <json>` / `--args -` escape hatch (it no longer
|
|
// applies), while a tool without one keeps it.
|
|
|
|
const toolsClientMock = vi.hoisted(() => ({
|
|
fetchTool: vi.fn(),
|
|
callTool: vi.fn(),
|
|
}));
|
|
|
|
const telemetryMock = vi.hoisted(() => ({
|
|
init: vi.fn(),
|
|
shutdown: vi.fn(async () => undefined),
|
|
track: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@argent/tools-client", () => ({
|
|
createToolsClient: vi.fn(() => toolsClientMock),
|
|
}));
|
|
|
|
vi.mock("@argent/telemetry", () => telemetryMock);
|
|
|
|
// A tool (like flow-add-step) that owns its `args` field.
|
|
const flowAddStepMeta = {
|
|
name: "flow-add-step",
|
|
description: "Add a step to the active flow recording",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
command: { type: "string" },
|
|
args: { type: "string" },
|
|
delayMs: { type: "integer" },
|
|
},
|
|
required: ["command"],
|
|
},
|
|
};
|
|
|
|
// A tool (like gesture-tap) with NO `args` field.
|
|
const gestureTapMeta = {
|
|
name: "gesture-tap",
|
|
description: "Tap the screen",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
udid: { type: "string" },
|
|
x: { type: "number" },
|
|
y: { type: "number" },
|
|
},
|
|
required: ["udid", "x", "y"],
|
|
},
|
|
};
|
|
|
|
// Unique text from the two suppressible whole-payload help lines.
|
|
const WHOLE_PAYLOAD_LINE = "Pass the entire payload as JSON";
|
|
const STDIN_SENTINEL_LINE = "Read the entire payload as JSON from stdin";
|
|
|
|
describe("argent run --help — whole-payload --args advertisement", () => {
|
|
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
function capturedHelp(): string {
|
|
return logSpy.mock.calls.map((c: unknown[]) => String(c[0] ?? "")).join("\n");
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
logSpy.mockRestore();
|
|
});
|
|
|
|
it("advertises the whole-payload --args escape hatch for a tool without its own `args` field", async () => {
|
|
toolsClientMock.fetchTool.mockResolvedValue(gestureTapMeta);
|
|
|
|
await run(["gesture-tap", "--help"], { paths: {} as never });
|
|
|
|
const help = capturedHelp();
|
|
expect(help).toContain(WHOLE_PAYLOAD_LINE);
|
|
expect(help).toContain(STDIN_SENTINEL_LINE);
|
|
// The tool call must never happen on the help path.
|
|
expect(toolsClientMock.callTool).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("suppresses the whole-payload --args lines for a tool that declares its own `args` field", async () => {
|
|
toolsClientMock.fetchTool.mockResolvedValue(flowAddStepMeta);
|
|
|
|
await run(["flow-add-step", "--help"], { paths: {} as never });
|
|
|
|
const help = capturedHelp();
|
|
expect(help).not.toContain(WHOLE_PAYLOAD_LINE);
|
|
expect(help).not.toContain(STDIN_SENTINEL_LINE);
|
|
// Its own `args` field is still shown as a per-field flag in the schema
|
|
// block (rendered as `--args <value>` by formatSchemaUsage), so suppression
|
|
// removes the whole-payload hatch without hiding the field itself.
|
|
expect(help).toContain("--args <value>");
|
|
expect(toolsClientMock.callTool).not.toHaveBeenCalled();
|
|
});
|
|
});
|