From 5f5d8ac3d965e7e19a9db3cbc77c553322d6e6ac Mon Sep 17 00:00:00 2001 From: Chuck Meyer Date: Wed, 26 Aug 2026 20:53:37 -0400 Subject: [PATCH 1/3] feat(create): accept a native config, making snapshots a clone source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of #14, completing the plan. `create` now detects a native config the same way `update` does and sends it as the file describes, so a snapshot is a complete clone source rather than only a way to manage the agent it came from. No guard is needed: create has no prior state to lose. Two deliberate divergences from the file: - status is forced to draft. Creating from a snapshot of a live agent should not silently publish the copy, and create is documented as producing a draft. - providerId is required. A native config carries it directly and there is no provider-name resolution on this path, so a missing one is an error pointing at `algolia-agent providers` rather than a confusing API rejection. Reuses _native_payload, so the literal-prompt rule and the rejection of --index/--replica/--provider/--var are shared with update rather than reimplemented. Its --var message now says "run the command again" instead of naming update. The friendly path is untouched: it still resolves a provider name and builds one tool, covered by a regression test. Verified live by cloning the template-created shopping-assistant agent. The copy differed from its source in nothing but identity and the forced draft status — tools, mode, allowUnlistedIndices, searchControls, all seven config keys, templateType, claude-fable-5, and a literal {{INSERT_BRAND}} all carried — and the clone's own snapshot round-tripped clean. Clone deleted afterwards. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 20 ++++++++ src/algolia_agent/cli.py | 58 +++++++++++++++++---- tests/test_cli.py | 105 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5b42043..2b66bef 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,26 @@ overwrite existing files without `--force`, and warns before replacing a prompt containing `{{template}}` variables — a snapshot holds rendered text and cannot recover a local template. +### Cloning an agent + +Because `create` also accepts a native config, a snapshot is a complete clone source: + +```bash +algolia-agent snapshot -o clone/agent-config.json +# edit the name in clone/agent-config.json +algolia-agent create --config clone/agent-config.json +``` + +The copy carries everything the friendly format cannot express — extra tools, `mode`, +`allowUnlistedIndices`, per-index `searchControls`, the full `config` block. Two +differences by design: `status` is forced to `draft`, since creating from a snapshot of a +live agent should not silently publish the copy, and a native config must carry +`providerId` directly (there is no provider-name lookup on that path — use +`algolia-agent providers` to find one). + +Snapshot each agent into its own directory. `PROMPT.md` is the default prompt filename, +so two snapshots in one directory collide — `snapshot` refuses rather than overwriting. + Agent Studio's own templates ship prompts containing placeholders such as `{{INSERT_BRAND}}` and `{{INSERT_LANGUAGE}}`. A snapshot preserves them verbatim, since a native config is literal — fill them in by editing `PROMPT.md` and running `update` diff --git a/src/algolia_agent/cli.py b/src/algolia_agent/cli.py index 3871c9a..e71b90e 100644 --- a/src/algolia_agent/cli.py +++ b/src/algolia_agent/cli.py @@ -195,10 +195,24 @@ def cmd_providers(client: AlgoliaAgentClient, args: argparse.Namespace): print() +def _report_created(agent: dict, args: argparse.Namespace): + if args.json: + print(json.dumps({"id": agent["id"], "name": agent["name"], "status": agent["status"]})) + return + print(f"Created agent: {agent['name']}") + print(f"Agent ID: {agent['id']}") + print(f"Status: {agent['status']}") + print(f"\nTo publish: algolia-agent publish {agent['id']}") + + def cmd_create(client: AlgoliaAgentClient, args: argparse.Namespace): # Load and merge config; auto-detect agent-config.json if --config not given config_path = args.config or (Path("agent-config.json") if Path("agent-config.json").exists() else None) file_config = load_config(config_path) if config_path else {} + if is_native_config(file_config): + _create_from_native(client, args, file_config, config_path) + return + config = merge_config(file_config, args) # Auto-detect PROMPT.md if instructions not specified @@ -272,16 +286,7 @@ def cmd_create(client: AlgoliaAgentClient, args: argparse.Namespace): if config.get("config"): payload["config"] = config["config"] - agent = client.create_agent(payload) - - if args.json: - print(json.dumps({"id": agent["id"], "name": agent["name"], "status": agent["status"]})) - return - - print(f"Created agent: {agent['name']}") - print(f"Agent ID: {agent['id']}") - print(f"Status: {agent['status']}") - print(f"\nTo publish: algolia-agent publish {agent['id']}") + _report_created(client.create_agent(payload), args) _MISSING = object() @@ -669,7 +674,7 @@ def _native_payload(config: dict, config_path, args) -> dict: "own templates ship placeholders like {{INSERT_BRAND}}, and substituting them\n" "on every update would overwrite them.\n\n" f"To fill them in, edit {config.get('instructions') or 'the prompt file'} " - "directly and run update again.\n" + "directly and run the command again.\n" "For templating across several agents, use the friendly config format " "(index/replicas)." ) @@ -704,6 +709,37 @@ def _read(field: str) -> str: return payload +def _create_from_native(client, args, file_config: dict, config_path): + """Create an agent from a native config — a snapshot, usually of another agent. + + Unlike update there is no prior state to lose, so no guard is needed. The payload + is sent as the file describes it, with one exception: status is forced to draft. + Creating from a snapshot of a live agent should not silently publish the copy, and + `create` is documented as producing a draft. + """ + payload = _native_payload(file_config, config_path, args) + + missing = [k for k in ("name", "providerId", "model") if not payload.get(k)] + if missing: + raise SystemExit( + f"ERROR: native config is missing required fields: {', '.join(missing)}\n" + "A native config carries providerId directly rather than a provider name; " + "run `algolia-agent providers` to look one up." + ) + + payload["status"] = "draft" + + if args.dry_run: + print("=== CREATE DRY RUN (native config) ===") + print(f" Source: {config_path}") + print(f" Tools: {', '.join(t.get('type') or '?' for t in payload.get('tools') or []) or 'none'}") + print("\nPayload:") + print(json.dumps(payload, indent=2)) + return + + _report_created(client.create_agent(payload), args) + + def _apply_update(client, args, current: dict, new_payload: dict, config_path=None, native: bool = False): """Dry-run, guard, send. Shared by the friendly and native config paths so both diff --git a/tests/test_cli.py b/tests/test_cli.py index fda2d1d..2e3cfc5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1866,3 +1866,108 @@ def test_snapshot_overwrite_hint_names_the_real_flags(tmp_path): assert "-o/--output" in msg assert "--instructions-file" in msg assert "-o/--instructions-file" not in msg + + +# ── native create (#14 phase 3) ─────────────────────────────────────────────── + +def _native_create_config(tmp_path, **overrides): + (tmp_path / "PROMPT.md").write_text("You are the {{INSERT_BRAND}} assistant.\n") + config = { + "name": "Shopping assistant", "providerId": "provider-uuid", + "model": "claude-fable-5", "status": "published", + "templateType": "shopping-assistant", "instructions": "PROMPT.md", + "tools": [{"name": "algolia_search_index", "type": "algolia_search_index", + "mode": "dynamic", "allowUnlistedIndices": True, + "indices": [{"index": "products", "description": "Catalog.", + "searchControls": {"distinct": {"exposed": True, "default": False}}}]}, + {"name": "algolia_display_results", "type": "algolia_display_results", + "isTerminal": False, "maxGroups": 3}], + "config": {"suggestions": {"enabled": True}, "memory": {"enabled": True}}, + } + config.update(overrides) + path = tmp_path / "agent-config.json" + path.write_text(json.dumps(config)) + return path + + +def test_create_from_native_sends_everything_and_forces_draft(tmp_path): + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path) + mock_client = MagicMock() + mock_client.create_agent.return_value = {"id": "new-uuid", "name": "Shopping assistant", + "status": "draft"} + cmd_create(mock_client, build_parser().parse_args(["create", "--config", str(path)])) + + payload = mock_client.create_agent.call_args.args[0] + # A snapshot of a live agent must not silently publish the copy. + assert payload["status"] == "draft" + # Everything the friendly format cannot express survives. + assert [t["type"] for t in payload["tools"]] == ["algolia_search_index", + "algolia_display_results"] + assert payload["tools"][0]["mode"] == "dynamic" + assert payload["tools"][0]["allowUnlistedIndices"] is True + assert payload["tools"][0]["indices"][0]["searchControls"] is not None + assert sorted(payload["config"]) == ["memory", "suggestions"] + assert payload["templateType"] == "shopping-assistant" + assert payload["providerId"] == "provider-uuid" + # No provider-name resolution happens on this path. + mock_client.resolve_provider_id.assert_not_called() + # Literal, not rendered. + assert "{{INSERT_BRAND}}" in payload["instructions"] + + +def test_create_from_native_requires_provider_id(tmp_path): + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path, providerId=None) + with pytest.raises(SystemExit) as exc: + cmd_create(MagicMock(), build_parser().parse_args(["create", "--config", str(path)])) + msg = str(exc.value) + assert "providerId" in msg + assert "algolia-agent providers" in msg + + +def test_create_from_native_dry_run_needs_no_client(tmp_path, capsys): + """_main calls cmd_create(None, args) for --dry-run, so this path must not use it.""" + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path) + cmd_create(None, build_parser().parse_args( + ["create", "--config", str(path), "--dry-run"])) + out = capsys.readouterr().out + assert "CREATE DRY RUN (native config)" in out + assert '"status": "draft"' in out + assert "algolia_display_results" in out + + +def test_create_from_native_rejects_friendly_flags(tmp_path): + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path) + for extra, expected in ([["--index", "p"], "--index"], [["--var", "INSERT_BRAND=x"], "--var"]): + with pytest.raises(SystemExit) as exc: + cmd_create(MagicMock(), build_parser().parse_args( + ["create", "--config", str(path)] + extra)) + assert expected in str(exc.value) + + +def test_create_friendly_path_unaffected(tmp_path): + """The friendly format must still resolve a provider name and build one tool.""" + from algolia_agent.cli import cmd_create + + (tmp_path / "PROMPT.md").write_text("Hello.") + path = tmp_path / "agent-config.json" + path.write_text(json.dumps({ + "name": "Friendly", "provider": "hackathon-gemini", "model": "gemini-3.5-flash", + "instructions": "PROMPT.md", "index": "products", + })) + mock_client = MagicMock() + mock_client.resolve_provider_id.return_value = "provider-uuid" + mock_client.create_agent.return_value = {"id": "x", "name": "Friendly", "status": "draft"} + cmd_create(mock_client, build_parser().parse_args(["create", "--config", str(path)])) + + mock_client.resolve_provider_id.assert_called_once_with("hackathon-gemini") + payload = mock_client.create_agent.call_args.args[0] + assert payload["providerId"] == "provider-uuid" + assert [t["type"] for t in payload["tools"]] == ["algolia_search_index"] From f26cb30fd6daaa79d28aebfddd801a38499b9a05 Mon Sep 17 00:00:00 2001 From: Chuck Meyer Date: Wed, 26 Aug 2026 20:56:10 -0400 Subject: [PATCH 2/3] feat(snapshot): never overwrite a templated prompt, even with --force MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A snapshot holds rendered server state, so replacing a templated prompt swaps {{placeholders}} for the values they resolved to. That loss is unrecoverable — the template only ever existed locally and rendered text cannot be un-rendered — so it is outside what --force should cover. Previously this was a stderr warning that proceeded anyway. The refusal names the variables it found and suggests --instructions-file .snapshot.md, which writes the rendered prompt beside the template rather than over it. Deleting the file remains the way to say you meant it. Plain prompt files are still overwritten by --force, because a snapshot can reproduce them. Verified against the repo's own templated TCG prompt: refused with --force, {{booth}} and {{event_name}} intact, and the suggested remedy writes alongside. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 ++++++++++---- src/algolia_agent/cli.py | 23 ++++++++++++------- tests/test_cli.py | 49 +++++++++++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2b66bef..60176df 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,19 @@ That round-trip is worth running after any snapshot: it is a completeness check. dry-run means the file holds everything the service does, so editing one value and applying it changes only that value. -`snapshot` also writes `SYSTEM.md` when the agent has a system prompt, refuses to -overwrite existing files without `--force`, and warns before replacing a prompt file -containing `{{template}}` variables — a snapshot holds rendered text and cannot recover -a local template. +`snapshot` also writes `SYSTEM.md` when the agent has a system prompt, and refuses to +overwrite existing files without `--force`. + +A prompt file containing `{{template}}` variables is never overwritten, **not even with +`--force`**: a snapshot holds rendered text, and a rendered prompt cannot be turned back +into a template. Write beside it instead: + +```bash +algolia-agent snapshot --instructions-file PROMPT.snapshot.md +``` + +Plain prompt files are overwritten by `--force` as normal, since a snapshot can +reproduce them. ### Cloning an agent diff --git a/src/algolia_agent/cli.py b/src/algolia_agent/cli.py index e71b90e..f4e7546 100644 --- a/src/algolia_agent/cli.py +++ b/src/algolia_agent/cli.py @@ -616,15 +616,22 @@ def cmd_snapshot(client: AlgoliaAgentClient, args: argparse.Namespace): ) # A snapshot holds rendered server state, so overwriting a templated prompt would - # replace {{placeholders}} with the values they resolved to — an unrecoverable loss, - # since the template only ever existed locally. + # replace {{placeholders}} with the values they resolved to. That loss is + # unrecoverable — the template only ever existed locally and rendered text cannot be + # un-rendered — so --force does not cover it. Overwriting a plain prompt is fine, + # because a snapshot can reproduce it. for p in existing: - if p.suffix.lower() == ".md" and extract_variables(p.read_text()): - print( - f"WARNING: {p} contains template variables and will be replaced with " - f"rendered text.\n The template exists only locally; a snapshot " - f"cannot recover it.", - file=sys.stderr, + if p.suffix.lower() != ".md": + continue + template_vars = extract_variables(p.read_text()) + if template_vars: + listed = ", ".join("{{" + v + "}}" for v in sorted(template_vars)) + raise SystemExit( + f"ERROR: {p} contains template variables ({listed}) and cannot be\n" + "recovered from rendered server state.\n\n" + "Write the prompt elsewhere with:\n" + f" --instructions-file {p.stem}.snapshot{p.suffix}\n" + f"or delete {p.name} first if you meant to replace it." ) snapshot = build_snapshot(agent, args.instructions_file, diff --git a/tests/test_cli.py b/tests/test_cli.py index 2e3cfc5..f233eac 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1665,21 +1665,58 @@ def test_cmd_snapshot_writes_both_files_and_refuses_to_clobber(tmp_path, capsys) assert "refusing to overwrite" in str(exc.value) -def test_cmd_snapshot_warns_before_replacing_a_templated_prompt(tmp_path, capsys): - """A local template cannot be recovered from rendered server state.""" +def test_cmd_snapshot_refuses_to_replace_a_templated_prompt_even_with_force(tmp_path): + """A local template cannot be recovered from rendered server state, so --force + does not cover it. Overwriting a plain prompt is fine — a snapshot reproduces it.""" from algolia_agent.cli import cmd_snapshot mock_client = MagicMock() mock_client.get_agent.return_value = _server_agent() out = tmp_path / "agent-config.json" out.write_text("{}") - (tmp_path / "PROMPT.md").write_text("Hello {{event_name}} from {{booth}}.") + prompt = tmp_path / "PROMPT.md" + prompt.write_text("Hello {{event_name}} from {{booth}}.") + + with pytest.raises(SystemExit) as exc: + cmd_snapshot(mock_client, build_parser().parse_args( + ["snapshot", "agent-uuid", "-o", str(out), "--force"])) + msg = str(exc.value) + assert "contains template variables" in msg + assert "{{event_name}}" in msg and "{{booth}}" in msg + assert "--instructions-file PROMPT.snapshot.md" in msg + # The template is still on disk, untouched. + assert prompt.read_text() == "Hello {{event_name}} from {{booth}}." + + +def test_cmd_snapshot_force_overwrites_a_plain_prompt(tmp_path): + from algolia_agent.cli import cmd_snapshot + + mock_client = MagicMock() + mock_client.get_agent.return_value = _server_agent() + out = tmp_path / "agent-config.json" + out.write_text("{}") + (tmp_path / "PROMPT.md").write_text("No variables here.") cmd_snapshot(mock_client, build_parser().parse_args( ["snapshot", "agent-uuid", "-o", str(out), "--force"])) - err = capsys.readouterr().err - assert "contains template variables" in err - assert "PROMPT.md" in err + assert (tmp_path / "PROMPT.md").read_text() == "Line one.\nLine two.\n" + + +def test_cmd_snapshot_templated_prompt_can_be_routed_elsewhere(tmp_path): + """The suggested remedy actually works: keep the template, write beside it.""" + from algolia_agent.cli import cmd_snapshot + + mock_client = MagicMock() + mock_client.get_agent.return_value = _server_agent() + out = tmp_path / "agent-config.json" + (tmp_path / "PROMPT.md").write_text("Hello {{event_name}}.") + + cmd_snapshot(mock_client, build_parser().parse_args( + ["snapshot", "agent-uuid", "-o", str(out), + "--instructions-file", "PROMPT.snapshot.md"])) + assert (tmp_path / "PROMPT.md").read_text() == "Hello {{event_name}}." + assert (tmp_path / "PROMPT.snapshot.md").read_text() == "Line one.\nLine two.\n" + assert json.loads(out.read_text())["instructions"] == "PROMPT.snapshot.md" def test_cmd_snapshot_no_warning_for_a_plain_prompt(tmp_path, capsys): From 5c933b0ad090a623e2b731ba98b73c4d2bcf37d3 Mon Sep 17 00:00:00 2001 From: Chuck Meyer Date: Wed, 26 Aug 2026 21:00:35 -0400 Subject: [PATCH 3/3] fix(snapshot): point at the flag that matches the templated file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on this PR. The templated-prompt refusal always suggested --instructions-file, but the templated file can be the system prompt, where --system-prompt-file is the remedy. The check now walks the known prompt targets paired with their flags instead of filtering on a .md suffix — which also catches a templated prompt under a non-.md name, since --instructions-file PROMPT.txt is legal and was slipping past. The native-create missing-fields error always cited providerId and pointed at `algolia-agent providers`, even when the missing field was name or model. It now names the config file to edit, mentions --name/--model when those are what is missing, and only raises providers for providerId. Co-Authored-By: Claude Opus 5 (1M context) --- src/algolia_agent/cli.py | 37 ++++++++++++++++-------- tests/test_cli.py | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/algolia_agent/cli.py b/src/algolia_agent/cli.py index f4e7546..0ad528e 100644 --- a/src/algolia_agent/cli.py +++ b/src/algolia_agent/cli.py @@ -620,18 +620,21 @@ def cmd_snapshot(client: AlgoliaAgentClient, args: argparse.Namespace): # unrecoverable — the template only ever existed locally and rendered text cannot be # un-rendered — so --force does not cover it. Overwriting a plain prompt is fine, # because a snapshot can reproduce it. - for p in existing: - if p.suffix.lower() != ".md": + prompt_targets = [(instr_path, "--instructions-file")] + if system_path: + prompt_targets.append((system_path, "--system-prompt-file")) + for path, flag in prompt_targets: + if not path.exists(): continue - template_vars = extract_variables(p.read_text()) + template_vars = extract_variables(path.read_text()) if template_vars: listed = ", ".join("{{" + v + "}}" for v in sorted(template_vars)) raise SystemExit( - f"ERROR: {p} contains template variables ({listed}) and cannot be\n" + f"ERROR: {path} contains template variables ({listed}) and cannot be\n" "recovered from rendered server state.\n\n" - "Write the prompt elsewhere with:\n" - f" --instructions-file {p.stem}.snapshot{p.suffix}\n" - f"or delete {p.name} first if you meant to replace it." + "Write it elsewhere with:\n" + f" {flag} {path.stem}.snapshot{path.suffix}\n" + f"or delete {path.name} first if you meant to replace it." ) snapshot = build_snapshot(agent, args.instructions_file, @@ -728,11 +731,21 @@ def _create_from_native(client, args, file_config: dict, config_path): missing = [k for k in ("name", "providerId", "model") if not payload.get(k)] if missing: - raise SystemExit( - f"ERROR: native config is missing required fields: {', '.join(missing)}\n" - "A native config carries providerId directly rather than a provider name; " - "run `algolia-agent providers` to look one up." - ) + hint = f"Add them to {config_path}" if config_path else "Add them to the config" + overridable = [k for k in missing if k in ("name", "model")] + if overridable: + flags = " or ".join(f"--{k}" for k in overridable) + hint += f", or pass {flags}" + lines = [ + f"ERROR: native config is missing required fields: {', '.join(missing)}", + hint + ".", + ] + if "providerId" in missing: + lines.append( + "A native config carries providerId directly rather than a provider " + "name — run `algolia-agent providers` to look one up." + ) + raise SystemExit("\n".join(lines)) payload["status"] = "draft" diff --git a/tests/test_cli.py b/tests/test_cli.py index f233eac..0a85546 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1684,6 +1684,7 @@ def test_cmd_snapshot_refuses_to_replace_a_templated_prompt_even_with_force(tmp_ assert "contains template variables" in msg assert "{{event_name}}" in msg and "{{booth}}" in msg assert "--instructions-file PROMPT.snapshot.md" in msg + assert "--system-prompt-file" not in msg # The template is still on disk, untouched. assert prompt.read_text() == "Hello {{event_name}} from {{booth}}." @@ -2008,3 +2009,64 @@ def test_create_friendly_path_unaffected(tmp_path): payload = mock_client.create_agent.call_args.args[0] assert payload["providerId"] == "provider-uuid" assert [t["type"] for t in payload["tools"]] == ["algolia_search_index"] + + +def test_snapshot_templated_system_prompt_points_at_the_right_flag(tmp_path): + """When the templated file is SYSTEM.md, --instructions-file is the wrong remedy.""" + from algolia_agent.cli import cmd_snapshot + + agent = _server_agent() + agent["systemPrompt"] = "You are terse." + mock_client = MagicMock() + mock_client.get_agent.return_value = agent + out = tmp_path / "agent-config.json" + (tmp_path / "SYSTEM.md").write_text("Act as {{persona}}.") + + with pytest.raises(SystemExit) as exc: + cmd_snapshot(mock_client, build_parser().parse_args( + ["snapshot", "agent-uuid", "-o", str(out), "--force"])) + msg = str(exc.value) + assert "SYSTEM.md" in msg + assert "{{persona}}" in msg + assert "--system-prompt-file SYSTEM.snapshot.md" in msg + assert "--instructions-file" not in msg + + +def test_snapshot_detects_a_templated_prompt_with_a_non_md_name(tmp_path): + """--instructions-file PROMPT.txt is legal, so keying on the .md suffix missed it.""" + from algolia_agent.cli import cmd_snapshot + + mock_client = MagicMock() + mock_client.get_agent.return_value = _server_agent() + out = tmp_path / "agent-config.json" + (tmp_path / "PROMPT.txt").write_text("Hello {{event_name}}.") + + with pytest.raises(SystemExit) as exc: + cmd_snapshot(mock_client, build_parser().parse_args( + ["snapshot", "agent-uuid", "-o", str(out), + "--instructions-file", "PROMPT.txt", "--force"])) + assert "--instructions-file PROMPT.snapshot.txt" in str(exc.value) + + +def test_native_create_missing_name_does_not_mention_providers(tmp_path): + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path, name=None) + with pytest.raises(SystemExit) as exc: + cmd_create(MagicMock(), build_parser().parse_args(["create", "--config", str(path)])) + msg = str(exc.value) + assert "name" in msg + assert "--name" in msg # actionable via a flag + assert "algolia-agent providers" not in msg # irrelevant remedy + assert "providerId" not in msg + + +def test_native_create_missing_provider_id_still_mentions_providers(tmp_path): + from algolia_agent.cli import cmd_create + + path = _native_create_config(tmp_path, providerId=None) + with pytest.raises(SystemExit) as exc: + cmd_create(MagicMock(), build_parser().parse_args(["create", "--config", str(path)])) + msg = str(exc.value) + assert "providerId" in msg + assert "algolia-agent providers" in msg