"""Integration tests for `basic-memory tool edit-note`.""" import json from typer.testing import CliRunner from basic_memory.cli.main import app as cli_app runner = CliRunner() def _write_note(title: str, folder: str, content: str, project: str | None = None) -> dict: args = [ "tool", "write-note", "--title", title, "--folder", folder, "--content", content, ] if project: args.extend(["--project", project]) result = runner.invoke(cli_app, args) assert result.exit_code == 0, result.output return json.loads(result.stdout) def _read_note(identifier: str, project: str | None = None) -> dict: args = ["tool", "read-note", identifier] if project: args.extend(["--project", project]) result = runner.invoke(cli_app, args) assert result.exit_code == 0, result.output return json.loads(result.stdout) def test_edit_note_append_success(app, app_config, test_project, config_manager): """append operation adds content to the end of the note.""" note = _write_note( "Edit Append Note", "edit-tests", "# Append\n\nBASE_APPEND_MARKER", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "append", "--content", "\nAPPENDED_MARKER", ], ) assert result.exit_code == 0, result.output updated = _read_note(note["permalink"]) assert updated["content"].index("APPENDED_MARKER") > updated["content"].index( "BASE_APPEND_MARKER" ) def test_edit_note_prepend_success(app, app_config, test_project, config_manager): """prepend operation inserts content before existing body content.""" note = _write_note( "Edit Prepend Note", "edit-tests", "# Prepend\n\nBASE_PREPEND_MARKER", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "prepend", "--content", "PREPENDED_MARKER\n", ], ) assert result.exit_code == 0, result.output updated = _read_note(note["permalink"]) assert updated["content"].index("PREPENDED_MARKER") < updated["content"].index( "BASE_PREPEND_MARKER" ) def test_edit_note_find_replace_success_with_expected_count( app, app_config, test_project, config_manager ): """find_replace succeeds when expected replacement count matches actual count.""" note = _write_note( "Edit Replace Note", "edit-tests", "# Replace\n\nFIND_ME_MARKER and FIND_ME_MARKER", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "find_replace", "--content", "REPLACED_MARKER", "--find-text", "FIND_ME_MARKER", "--expected-replacements", "2", ], ) assert result.exit_code == 0, result.output updated = _read_note(note["permalink"]) assert "FIND_ME_MARKER" not in updated["content"] assert updated["content"].count("REPLACED_MARKER") == 2 def test_edit_note_find_replace_fails_without_find_text( app, app_config, test_project, config_manager ): """find_replace requires --find-text.""" note = _write_note( "Edit Missing Find Note", "edit-tests", "# Missing Find\n\nOriginal", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "find_replace", "--content", "Replacement", ], ) assert result.exit_code != 0 assert "find_text parameter is required for find_replace operation" in result.output def test_edit_note_replace_section_success(app, app_config, test_project, config_manager): """replace_section updates exactly the targeted section body.""" note = _write_note( "Edit Section Note", "edit-tests", "# Header\n\n## Keep\nKeep body\n\n## Target Section\nOld section body\n\n## After\nAfter body", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "replace_section", "--content", "New section body", "--section", "## Target Section", ], ) assert result.exit_code == 0, result.output updated = _read_note(note["permalink"]) assert "New section body" in updated["content"] assert "Old section body" not in updated["content"] assert "## After" in updated["content"] def test_edit_note_replace_section_fails_without_section( app, app_config, test_project, config_manager ): """replace_section requires --section.""" note = _write_note( "Edit Missing Section Note", "edit-tests", "# Missing Section\n\nBody", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "replace_section", "--content", "Replacement body", ], ) assert result.exit_code != 0 assert "section parameter is required for section-based operations" in result.output def test_edit_note_append_creates_nonexistent_note_cli( app, app_config, test_project, config_manager ): """append to a non-existent note via CLI should auto-create and include fileCreated.""" result = runner.invoke( cli_app, [ "tool", "edit-note", "cli-tests/auto-created-note", "--operation", "append", "--content", "# Auto Created\n\nCreated via CLI append.", ], ) assert result.exit_code == 0, result.output data = json.loads(result.stdout) assert data["fileCreated"] is True assert data["operation"] == "append" assert data["title"] is not None # Verify the note is readable read_data = _read_note(data["permalink"]) assert "Auto Created" in read_data["content"] def test_edit_note_json_format_contract(app, app_config, test_project, config_manager): """JSON output returns metadata keys required by contract.""" note = _write_note( "Edit JSON Note", "edit-tests", "# JSON\n\nBody", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "append", "--content", "\nJSON_MARKER", ], ) assert result.exit_code == 0, result.output data = json.loads(result.stdout) assert set(data.keys()) == { "title", "permalink", "file_path", "operation", "checksum", "fileCreated", } assert data["operation"] == "append" assert data["fileCreated"] is False assert data["title"] == "Edit JSON Note" def test_edit_note_backend_failure_returns_nonzero(app, app_config, test_project, config_manager): """Edit should return non-zero when backend edit operation fails.""" note = _write_note( "Edit Backend Failure Note", "edit-tests", "# Failure\n\nGamma", ) result = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "find_replace", "--find-text", "Gamma", "--content", "Delta", "--expected-replacements", "2", ], ) assert result.exit_code != 0 def test_edit_note_project_and_routing_flag_parity(app, app_config, test_project, config_manager): """edit-note supports --project/--local and validates --local/--cloud conflict.""" note = _write_note( "Edit Project Flag Note", "edit-tests", "# Project Flag\n\nPROJECT_FLAG_MARKER", project=test_project.name, ) success = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "append", "--content", "\nPROJECT_UPDATE_MARKER", "--project", test_project.name, "--local", ], ) assert success.exit_code == 0, success.output assert "No such option" not in success.output updated = _read_note(note["permalink"], project=test_project.name) assert "PROJECT_UPDATE_MARKER" in updated["content"] conflict = runner.invoke( cli_app, [ "tool", "edit-note", note["permalink"], "--operation", "append", "--content", "ignored", "--local", "--cloud", ], ) assert conflict.exit_code != 0 assert "Cannot specify both --local and --cloud" in conflict.output