diff --git a/.github/bunny-review/bunny_review.py b/.github/bunny-review/bunny_review.py index 9ec80d1b7..bc1382c5c 100644 --- a/.github/bunny-review/bunny_review.py +++ b/.github/bunny-review/bunny_review.py @@ -518,6 +518,26 @@ def model_call(client, messages, stats): return resp.choices[0].message.content or "" +def extract_json_or_repair(client, messages, content, stats): + try: + return extract_json(content) + except ValueError: + repair_messages = [ + *messages, + {"role": "assistant", "content": content}, + { + "role": "user", + "content": ( + "The previous response did not contain a JSON object. Reply only " + "with FINAL_REVIEW followed by one JSON object matching the required " + "Bunny Review schema. Do not include prose, Markdown, or another " + "context request." + ), + }, + ] + return extract_json(model_call(client, repair_messages, stats)) + + def review_packet_with_model(client, skill, triage_content, stats): messages = [ {"role": "system", "content": skill}, @@ -526,7 +546,7 @@ def review_packet_with_model(client, skill, triage_content, stats): first_response = model_call(client, messages, stats) request = parse_context_request(first_response) if request is None: - return extract_json(first_response) + return extract_json_or_repair(client, messages, first_response, stats) extra_context = build_extra_context(request, stats) final_messages = [ {"role": "system", "content": skill}, @@ -541,7 +561,8 @@ def review_packet_with_model(client, skill, triage_content, stats): ), }, ] - return extract_json(model_call(client, final_messages, stats)) + final_response = model_call(client, final_messages, stats) + return extract_json_or_repair(client, final_messages, final_response, stats) def skeptical_review_pass(client, skill, triage_content, stats): @@ -561,7 +582,8 @@ def skeptical_review_pass(client, skill, triage_content, stats): {"role": "user", "content": triage_content}, {"role": "user", "content": audit_prompt}, ] - return extract_json(model_call(client, messages, stats)) + response = model_call(client, messages, stats) + return extract_json_or_repair(client, messages, response, stats) def judge_review_pass(client, skill, triage_content, broad_review, skeptical_review, stats): @@ -581,7 +603,8 @@ def judge_review_pass(client, skill, triage_content, broad_review, skeptical_rev {"role": "user", "content": triage_content}, {"role": "user", "content": judge_prompt}, ] - return extract_json(model_call(client, messages, stats)) + response = model_call(client, messages, stats) + return extract_json_or_repair(client, messages, response, stats) def three_pass_review(client, skill, triage_content, stats): @@ -703,13 +726,20 @@ def validate_findings(review_obj, base): if finding.severity not in severities: finding.severity = "medium" if not finding.path or finding.path not in allowed: - invalid.append(f"{finding.path or ''}: not in changed files") + invalid.append( + f"{finding.severity} '{finding.title or ''}' at " + f"{finding.path or ''}: not in changed files" + ) continue if not isinstance(finding.line, int): - invalid.append(f"{finding.path}: missing integer line for '{finding.title}'") + invalid.append( + f"{finding.severity} '{finding.title or ''}' at " + f"{finding.path}: missing integer line" + ) continue if finding.line not in allowed.get(finding.path, set()): invalid.append( + f"{finding.severity} '{finding.title or ''}' at " f"{finding.path}:{finding.line}: line is not an added/changed diff line" ) continue @@ -764,12 +794,12 @@ def commit_subject(head_sha): return " ".join(result.stdout.split()) -def commit_line(head_sha, message=None): +def commit_line(head_sha, message=None, label="Commit"): subject = " ".join(str(message or "").split()) or commit_subject(head_sha) ref = short_ref(head_sha) if subject: - return f"Commit: {ref} - {subject}" - return f"Commit: {ref}" + return f"{label}: {ref} - {subject}" + return f"{label}: {ref}" def md_cell(value): @@ -830,11 +860,20 @@ def finding_summary(findings): return f"{len(findings)} finding(s): " + ", ".join(pieces) +def has_failed_review_check(pre_merge): + return any( + str(item.get("name", "")).strip().lower() == "review failed" + and status_meta(item.get("status"))["label"] == "FAIL" + for item in pre_merge + ) + + def review_callout(findings, pre_merge): has_blocking = any( severity_meta(finding.severity)["rank"] <= severity_meta("high")["rank"] for finding in findings ) + review_failed = has_failed_review_check(pre_merge) has_failed_check = any( status_meta(item.get("status"))["label"] == "FAIL" for item in pre_merge ) @@ -843,6 +882,14 @@ def review_callout(findings, pre_merge): for item in pre_merge ) summary = finding_summary(findings) + if review_failed and not findings: + return "\n".join( + [ + "> [!CAUTION]", + "> **Specimen unexamined.** Bunny Review did not complete, so no model findings are available.", + "> Repair the failed review control or rerun Bunny before treating this PR as reviewed.", + ] + ) if has_blocking or has_failed_check: return "\n".join( [ @@ -877,8 +924,8 @@ def render_review_metadata(review_obj, head_sha): [ "> [!NOTE]", f"> Mode: `{mode}` ", - f"> {commit_line(head_sha, commit_message)} ", - f"> Base: `{short_ref(base)}`", + f"> {commit_line(head_sha, commit_message, label='Head')} ", + f"> {commit_line(base, label='Base')}", ] ) @@ -905,11 +952,7 @@ def agent_prompt_for_finding(finding): def render_agent_prompt(findings): - sections = [ - "Use this as an implementation handoff, not as reviewer prose. Keep the response " - "concise, technical, and direct.", - ] - sections.extend(agent_prompt_for_finding(finding) for finding in findings) + sections = [agent_prompt_for_finding(finding) for finding in findings] return code_block_text("\n\n".join(sections)) @@ -1018,9 +1061,14 @@ def render_walkthrough(review_obj, findings, invalid_findings, ci_status, head_s pre_merge = [item for item in pre_merge if not is_stale_ci_check(item)] checked = [item for item in checked if not is_stale_ci_text(str(item))] pre_merge = ci_status_to_pre_merge_checks(normalized_ci_status) + pre_merge + state_marker = ( + f"" + if head_sha and not has_failed_review_check(pre_merge) + else "" + ) body = [ BUNNY_MARKER, - f"", + state_marker, "## 🐰 Bunny Review", "", review_callout(findings, pre_merge), @@ -1047,7 +1095,16 @@ def render_walkthrough(review_obj, findings, invalid_findings, ci_status, head_s f"{md_cell(finding.title)} |" ) else: - body.extend(["", "> [!TIP]", "> No actionable defects isolated."]) + if has_failed_review_check(pre_merge): + body.extend( + [ + "", + "> [!CAUTION]", + "> No model findings are available because Bunny Review failed before completing inspection.", + ] + ) + else: + body.extend(["", "> [!TIP]", "> No actionable defects isolated."]) agent_prompt = render_agent_prompt_details( findings, "🤖 Repair prompt for isolated Bunny findings" ) @@ -1086,6 +1143,7 @@ def render_walkthrough(review_obj, findings, invalid_findings, ci_status, head_s f"> Withheld {len(invalid_findings)} model finding(s) because their diff locations failed validation.", ] ) + body.extend([f"- {note}" for note in invalid_findings[:5]]) if normalized_ci_status: body.extend(["", "### 🧰 CI Status", normalized_ci_status]) return "\n".join(body).strip() + "\n" @@ -1151,11 +1209,91 @@ def model_failure_detail(exc): ) +def current_head_sha(): + result = run(["git", "rev-parse", "HEAD"], timeout=30, check=True) + return result.stdout.strip() + + +def ensure_local_head(head_sha, pr_num): + if not head_sha or current_head_sha() == head_sha: + return + if pr_num: + run( + [ + "git", + "fetch", + "--force", + "origin", + f"pull/{pr_num}/head:refs/remotes/bunny-review/pr-{pr_num}", + ], + timeout=120, + ) + checkout = run(["git", "checkout", "--detach", head_sha], timeout=90) + if checkout.returncode != 0: + raise RuntimeError( + "Local checkout does not contain the PR head GitHub reported: " + f"{head_sha}\n{checkout.stdout}{checkout.stderr}" + ) + actual = current_head_sha() + if actual != head_sha: + raise RuntimeError(f"Local checkout is {actual}, expected PR head {head_sha}") + + +def issue_comments(pr_num): + gh = run_gh( + [ + "api", + f"repos/{os.environ['GITHUB_REPOSITORY']}/issues/{pr_num}/comments?per_page=100", + "--paginate", + ], + check=True, + ) + return load_json_list(gh.stdout) + + +def sorted_walkthrough_comments(pr_num): + walkthroughs = [ + comment for comment in issue_comments(pr_num) if BUNNY_MARKER in comment.get("body", "") + ] + return sorted( + walkthroughs, + key=lambda comment: ( + comment.get("updated_at") or "", + comment.get("created_at") or "", + comment.get("id") or 0, + ), + ) + + +def latest_walkthrough_comment(pr_num): + walkthroughs = sorted_walkthrough_comments(pr_num) + if not walkthroughs: + return None + return walkthroughs[-1] + + +def is_completed_review_body(body): + if not STATE_MARKER_RE.search(body): + return False + lowered = body.lower() + failed_markers = ( + "review failed", + "specimen unexamined", + "could not complete", + "no model findings are available", + "review skipped", + ) + return not any(marker in lowered for marker in failed_markers) + + def discover_last_reviewed_sha(pr_num): - gh = run_gh(["pr", "view", pr_num, "--json", "comments", "--jq", ".comments[].body"]) - matches = STATE_MARKER_RE.findall(gh.stdout) - if matches: - return matches[-1] + for comment in reversed(sorted_walkthrough_comments(pr_num)): + body = comment.get("body", "") + if not is_completed_review_body(body): + continue + matches = STATE_MARKER_RE.findall(body) + if matches: + return matches[-1] return None @@ -1182,7 +1320,8 @@ def resolve_review_base(pr_num, requested_mode): previous = discover_last_reviewed_sha(pr_num) if previous: exists = run(["git", "cat-file", "-e", f"{previous}^{{commit}}"]) - if exists.returncode == 0: + ancestor = run(["git", "merge-base", "--is-ancestor", previous, head_sha]) + if exists.returncode == 0 and ancestor.returncode == 0: return previous, base_ref, head_sha, "incremental" return f"origin/{base_ref}", base_ref, head_sha, "full" @@ -1210,6 +1349,7 @@ def produce_review(args): pr_num = os.environ.get("PR_NUM", "") requested_mode = args.mode or parse_command_mode() base, base_ref, head_sha, effective_mode = resolve_review_base(pr_num, requested_mode) + ensure_local_head(head_sha, pr_num) patch_command_status_running(pr_num, head_sha, effective_mode) ci_status = os.environ.get("CI_STATUS", "") files = changed_files(base) @@ -1354,40 +1494,14 @@ def render_review(args): def find_walkthrough_comment(pr_num): - gh = run_gh( - [ - "api", - f"repos/{os.environ['GITHUB_REPOSITORY']}/issues/{pr_num}/comments?per_page=100", - "--paginate", - ], - check=True, - ) - try: - comments = json.loads(gh.stdout or "[]") - except json.JSONDecodeError: - comments = [] - for line in gh.stdout.splitlines(): - if not line.strip(): - continue - loaded = json.loads(line) - if isinstance(loaded, list): - comments.extend(loaded) - for comment in comments: - if BUNNY_MARKER in comment.get("body", ""): - return comment.get("id") + comment = latest_walkthrough_comment(pr_num) + if comment: + return comment.get("id") return None def find_command_status_comment(pr_num): - gh = run_gh( - [ - "api", - f"repos/{os.environ['GITHUB_REPOSITORY']}/issues/{pr_num}/comments?per_page=100", - "--paginate", - ], - check=True, - ) - for comment in load_json_list(gh.stdout): + for comment in issue_comments(pr_num): if COMMAND_STATUS_MARKER in comment.get("body", ""): return comment.get("id") return None @@ -1508,7 +1622,9 @@ def post_review(args): pr_num = os.environ["PR_NUM"] body = pathlib.Path(args.review_md).read_text("utf-8") head_sha_match = STATE_MARKER_RE.search(body) - head_sha = head_sha_match.group(1) if head_sha_match else "" + head_sha = head_sha_match.group(1) if head_sha_match else os.environ.get( + "PR_HEAD_SHA", "" + ) comment_id = find_walkthrough_comment(pr_num) if comment_id: run_gh( diff --git a/.github/workflows/bunny-review-auto.yml b/.github/workflows/bunny-review-auto.yml index 90f86dd80..a22b941ea 100644 --- a/.github/workflows/bunny-review-auto.yml +++ b/.github/workflows/bunny-review-auto.yml @@ -2,7 +2,7 @@ name: Bunny Review Auto Dispatch on: pull_request_target: - types: [opened, reopened, synchronize, ready_for_review] + types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] permissions: actions: write @@ -16,8 +16,7 @@ concurrency: jobs: dispatch: if: > - github.event.pull_request.base.ref == 'refactor' && - github.event.pull_request.draft == false + github.event.pull_request.base.ref == 'refactor' runs-on: ubuntu-latest steps: - name: Dispatch trusted Bunny reviewer diff --git a/.github/workflows/bunny-review.yml b/.github/workflows/bunny-review.yml index 237e07422..4f7924ee8 100644 --- a/.github/workflows/bunny-review.yml +++ b/.github/workflows/bunny-review.yml @@ -30,7 +30,7 @@ permissions: pull-requests: write issues: write actions: read # needed to read CI status - checks: write + statuses: write concurrency: group: bunny-review-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }} @@ -83,21 +83,17 @@ jobs: HEAD_SHA=$(gh pr view "$PR_NUM" --json headRefOid -q .headRefOid) echo "PR_HEAD_SHA=$HEAD_SHA" >> "$GITHUB_ENV" - - name: Mark Bunny check in progress + - name: Mark Bunny status in progress env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh api \ --method POST \ - "repos/${{ github.repository }}/check-runs" \ - -f name="Bunny Review" \ - -f head_sha="$PR_HEAD_SHA" \ - -f status="in_progress" \ - -f details_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f external_id="bunny-review-${PR_NUM}" \ - -f "output[title]=Bunny Review" \ - -f "output[summary]=The trusted Bunny reviewer is inspecting this pull request." \ - --jq .id > bunny-check-run-id.txt + "repos/${{ github.repository }}/statuses/$PR_HEAD_SHA" \ + -f state="pending" \ + -f context="Bunny Review" \ + -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + -f description="The trusted Bunny reviewer is inspecting this pull request." >/dev/null - uses: actions/setup-python@v5 with: @@ -116,10 +112,15 @@ jobs: BUNNY_REVIEW_SKILL_PATH: /tmp/bunny-review-tool/.github/bunny-review/SKILL.md GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + HEAD_SHA=$(gh pr view "$PR_NUM" --json headRefOid -q .headRefOid) + if [ "$HEAD_SHA" != "$(git rev-parse HEAD)" ]; then + git fetch --force origin "pull/$PR_NUM/head:refs/remotes/bunny-review/pr-$PR_NUM" + git checkout --detach "$HEAD_SHA" + fi + python /tmp/bunny-review-tool/.github/bunny-review/bunny_review.py produce & BUNNY_PID=$! - HEAD_SHA=$(gh pr view "$PR_NUM" --json headRefOid -q .headRefOid) TARGET_CHECKS='Frontend|Rust|Smoke' FOUND_ANY=0 MISSING_CHECK_ATTEMPTS=18 @@ -182,40 +183,27 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: python /tmp/bunny-review-tool/.github/bunny-review/bunny_review.py post - - name: Complete Bunny check + - name: Complete Bunny status if: always() env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - CHECK_RUN_ID="" - if [ -f bunny-check-run-id.txt ]; then - CHECK_RUN_ID="$(cat bunny-check-run-id.txt)" - fi - if [ -z "$CHECK_RUN_ID" ]; then - CHECK_RUN_ID="$(gh api "repos/${{ github.repository }}/commits/$PR_HEAD_SHA/check-runs" \ - --jq '.check_runs[] | select(.name == "Bunny Review") | .id' | tail -n 1)" - fi - if [ -z "$CHECK_RUN_ID" ]; then - echo "::warning::No Bunny check run id found; unable to complete custom check." - exit 0 - fi - if [ "${{ job.status }}" = "success" ]; then - CONCLUSION="success" - TITLE="Bunny Review Complete" - SUMMARY="Bunny posted or updated its review for this pull request." + STATE="success" + DESCRIPTION="Bunny posted or updated its review for this pull request." else - CONCLUSION="failure" - TITLE="Bunny Review Failed" - SUMMARY="Bunny Review did not complete. Inspect the trusted workflow run for details." + STATE="failure" + DESCRIPTION="Bunny Review did not complete. Inspect the trusted workflow run for details." + fi + if [ -f review.json ] && python -c 'import json, pathlib, sys; data=json.loads(pathlib.Path("review.json").read_text()); failed=any(str(item.get("name","")).lower()=="review failed" and str(item.get("status","")).lower() in {"fail","failed","failure"} for item in data.get("pre_merge_checks", [])); sys.exit(0 if failed else 1)'; then + STATE="failure" + DESCRIPTION="Bunny Review posted a failure report; rerun after repairing the review control." fi gh api \ - --method PATCH \ - "repos/${{ github.repository }}/check-runs/$CHECK_RUN_ID" \ - -f status="completed" \ - -f conclusion="$CONCLUSION" \ - -f completed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - -f details_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f "output[title]=$TITLE" \ - -f "output[summary]=$SUMMARY" >/dev/null + --method POST \ + "repos/${{ github.repository }}/statuses/$PR_HEAD_SHA" \ + -f state="$STATE" \ + -f context="Bunny Review" \ + -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + -f description="$DESCRIPTION" >/dev/null diff --git a/src-tauri/src/commands/storage/commands/entities.rs b/src-tauri/src/commands/storage/commands/entities.rs index 950fcca3c..ede5ceac7 100644 --- a/src-tauri/src/commands/storage/commands/entities.rs +++ b/src-tauri/src/commands/storage/commands/entities.rs @@ -1131,13 +1131,8 @@ mod tests { ) .expect("supported create should succeed"); - let read = storage_get_inner( - &state, - "characters".to_string(), - "char-1".to_string(), - None, - ) - .expect("supported get should succeed"); + let read = storage_get_inner(&state, "characters".to_string(), "char-1".to_string(), None) + .expect("supported get should succeed"); assert_eq!(read["id"], "char-1"); } diff --git a/src-tauri/src/http_dispatch.rs b/src-tauri/src/http_dispatch.rs index 333ca7c36..ad2d9a5e6 100644 --- a/src-tauri/src/http_dispatch.rs +++ b/src-tauri/src/http_dispatch.rs @@ -1,8 +1,8 @@ use crate::state::AppState; use crate::storage_commands::{ - admin, agents, avatars, backgrounds, backup, bot_browser, characters, chats, - custom_tools, entity_commands, exports, fonts, game_assets, game_state_snapshots, generation, - http, images, imports, integrations, knowledge, llm, lorebook_images, mari, personas, profile, + admin, agents, avatars, backgrounds, backup, bot_browser, characters, chats, custom_tools, + entity_commands, exports, fonts, game_assets, game_state_snapshots, generation, http, images, + imports, integrations, knowledge, llm, lorebook_images, mari, personas, profile, profile_commands, prompts, shared, sprites, translation, updates, }; use marinara_core::{AppError, AppResult}; @@ -1144,6 +1144,15 @@ mod tests { }) } + fn default_for_agents(state: &AppState, id: &str) -> bool { + state + .storage + .get("connections", id) + .expect("connection should read") + .and_then(|row| row.get("defaultForAgents").and_then(Value::as_bool)) + .unwrap_or(false) + } + fn quoted_commands(source: &str) -> BTreeSet { source .split('"') @@ -1335,6 +1344,110 @@ mod tests { ); } + #[tokio::test] + async fn dispatch_storage_create_connection_clears_previous_agent_default() { + let state = test_state("storage-create-connection-agent-default"); + for (id, provider) in [("language-a", "anthropic"), ("language-b", "openai")] { + dispatch( + &state, + InvokeRequest { + command: "storage_create".to_string(), + args: Some(json!({ + "entity": "connections", + "value": { + "id": id, + "name": id, + "provider": provider, + "defaultForAgents": true + } + })), + }, + ) + .await + .expect("remote connection create should dispatch"); + } + + assert!(!default_for_agents(&state, "language-a")); + assert!(default_for_agents(&state, "language-b")); + } + + #[tokio::test] + async fn dispatch_storage_update_connection_clears_previous_agent_default() { + let state = test_state("storage-update-connection-agent-default"); + for (id, default_for_agents) in [("language-a", true), ("language-b", false)] { + state + .storage + .create( + "connections", + json!({ + "id": id, + "name": id, + "provider": "openai", + "defaultForAgents": default_for_agents + }), + ) + .expect("connection should be seeded"); + } + + dispatch( + &state, + InvokeRequest { + command: "storage_update".to_string(), + args: Some(json!({ + "entity": "connections", + "id": "language-b", + "patch": { "defaultForAgents": true } + })), + }, + ) + .await + .expect("remote connection update should dispatch"); + + assert!(!default_for_agents(&state, "language-a")); + assert!(default_for_agents(&state, "language-b")); + } + + #[tokio::test] + async fn dispatch_storage_update_protects_default_chat_preset_fields() { + let state = test_state("storage-update-default-chat-preset"); + state + .storage + .create( + "chat-presets", + json!({ + "id": "default-chat-preset", + "name": "Default Chat", + "mode": "chat", + "isDefault": true, + "isActive": true + }), + ) + .expect("default chat preset should be seeded"); + + let error = dispatch( + &state, + InvokeRequest { + command: "storage_update".to_string(), + args: Some(json!({ + "entity": "chat-presets", + "id": "default-chat-preset", + "patch": { "name": "Mutated Default" } + })), + }, + ) + .await + .expect_err("default chat preset field mutations should be rejected remotely"); + + assert_eq!(error.code, "invalid_input"); + assert_eq!(error.message, "Default chat presets cannot be updated"); + let preset = state + .storage + .get("chat-presets", "default-chat-preset") + .expect("chat preset should read") + .expect("chat preset should still exist"); + assert_eq!(preset["name"], "Default Chat"); + } + #[tokio::test] async fn dispatch_supports_remote_chat_gallery_upload() { let state = test_state("chat-gallery-upload"); @@ -1596,87 +1709,6 @@ mod tests { .is_some()); } - #[tokio::test] - async fn dispatch_storage_create_connection_clears_previous_agent_default() { - let state = test_state("remote-connection-default-create"); - for (id, provider) in [("language-a", "anthropic"), ("language-b", "openai")] { - dispatch( - &state, - InvokeRequest { - command: "storage_create".to_string(), - args: Some(json!({ - "entity": "connections", - "value": { - "id": id, - "name": id, - "provider": provider, - "defaultForAgents": true - } - })), - }, - ) - .await - .expect("remote connection create should dispatch"); - } - - let language_a = state - .storage - .get("connections", "language-a") - .unwrap() - .unwrap(); - let language_b = state - .storage - .get("connections", "language-b") - .unwrap() - .unwrap(); - assert_eq!(language_a["defaultForAgents"], false); - assert_eq!(language_b["defaultForAgents"], true); - } - - #[tokio::test] - async fn dispatch_storage_update_rejects_default_chat_preset_mutation() { - let state = test_state("remote-default-preset-guard"); - state - .storage - .create( - "chat-presets", - json!({ - "id": "default-chat", - "name": "Default Chat", - "mode": "chat", - "isDefault": true, - "default": true, - "isActive": true, - "active": true, - "parameters": {}, - "settings": {} - }), - ) - .expect("default chat preset should be seeded"); - - let error = dispatch( - &state, - InvokeRequest { - command: "storage_update".to_string(), - args: Some(json!({ - "entity": "chat-presets", - "id": "default-chat", - "patch": { "name": "Mutated" } - })), - }, - ) - .await - .expect_err("remote default preset mutation should be rejected"); - - assert_eq!(error.code, "invalid_input"); - let preset = state - .storage - .get("chat-presets", "default-chat") - .unwrap() - .unwrap(); - assert_eq!(preset["name"], "Default Chat"); - } - #[tokio::test] async fn dispatch_supports_remote_update_apply_manual_path() { let state = test_state("update-apply");