Web source attribution {data.web_available ? "" : "(not configured)"}
+Web source attribution · {data.web_provider ? data.web_provider.charAt(0).toUpperCase() + data.web_provider.slice(1) : "unavailable"}
{data.web_available ? ( (data.web_matches?.length || 0) > 0 ? (✓ No matching sources found on the open web.
+✓ Checked the live web ({data.web_provider || "search"}) — no close matches for the flagged text.
) ) : ({data.web_summary}
@@ -418,6 +418,7 @@ function buildReplayProvenance(record) { web_matches: details.web_matches || [], web_summary: details.web_summary || "", web_available: details.web_available === true, + web_provider: details.web_provider || "", }; } diff --git a/backend/tests/test_services.py b/backend/tests/test_services.py index 65b696f7..f653ccc6 100644 --- a/backend/tests/test_services.py +++ b/backend/tests/test_services.py @@ -180,6 +180,8 @@ def test_scoring_high_ai_yields_review_verdict(): fingerprint_methodology, fuse_pcv_scores, ) +import types +from app.services import web_attribution from app.services.web_attribution import attribute_web_sources @@ -223,9 +225,35 @@ def test_pcv_fusion_bounded_and_reasonable(): assert 0 <= fused <= 100 -def test_web_attribution_degrades_without_key(monkeypatch): +def test_web_attribution_works_without_any_key(monkeypatch): + """Web attribution stays available with zero API keys via the DuckDuckGo fallback.""" monkeypatch.setenv("BRAVE_API_KEY", "") + monkeypatch.setenv("GOOGLE_CSE_API_KEY", "") + monkeypatch.delenv("GOOGLE_CSE_ID", raising=False) + + class _FakeResp: + status_code = 200 + text = ( + "" + "Example machine learning evaluation paper" + ) + + def _fake_post(url, **kwargs): + return _FakeResp() + + monkeypatch.setattr(web_attribution, "requests", types.SimpleNamespace(post=_fake_post, get=lambda *a, **k: _FakeResp())) result = attribute_web_sources(["a sentence about machine learning evaluation"]) - assert result.available is False - assert "not configured" in result.summary.lower() + assert result.available is True + assert result.provider == "duckduckgo" + assert result.segments_checked == 1 + assert len(result.matches) == 1 + assert result.matches[0].url == "https://example.com/paper" + + +def test_web_attribution_empty_segments_needs_no_provider(monkeypatch): + monkeypatch.setenv("BRAVE_API_KEY", "") + monkeypatch.setenv("GOOGLE_CSE_API_KEY", "") + result = attribute_web_sources([]) + assert result.available is True + assert result.matches == [] diff --git a/frontend/dist/dist/assets/index-BOzsEJay.js b/frontend/dist/dist/assets/index-BOzsEJay.js new file mode 100644 index 00000000..ea9b6e38 --- /dev/null +++ b/frontend/dist/dist/assets/index-BOzsEJay.js @@ -0,0 +1,63 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(s){if(s.ep)return;s.ep=!0;const o=n(s);fetch(s.href,o)}})();function Jg(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Ld={exports:{}},ri={},Fd={exports:{}},$={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ss=Symbol.for("react.element"),Zg=Symbol.for("react.portal"),ey=Symbol.for("react.fragment"),ty=Symbol.for("react.strict_mode"),ny=Symbol.for("react.profiler"),ry=Symbol.for("react.provider"),sy=Symbol.for("react.context"),oy=Symbol.for("react.forward_ref"),iy=Symbol.for("react.suspense"),ay=Symbol.for("react.memo"),ly=Symbol.for("react.lazy"),rc=Symbol.iterator;function uy(e){return e===null||typeof e!="object"?null:(e=rc&&e[rc]||e["@@iterator"],typeof e=="function"?e:null)}var Md={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ud=Object.assign,$d={};function hr(e,t,n){this.props=e,this.context=t,this.refs=$d,this.updater=n||Md}hr.prototype.isReactComponent={};hr.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};hr.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function zd(){}zd.prototype=hr.prototype;function jl(e,t,n){this.props=e,this.context=t,this.refs=$d,this.updater=n||Md}var Al=jl.prototype=new zd;Al.constructor=jl;Ud(Al,hr.prototype);Al.isPureReactComponent=!0;var sc=Array.isArray,Bd=Object.prototype.hasOwnProperty,Dl={current:null},Hd={key:!0,ref:!0,__self:!0,__source:!0};function Wd(e,t,n){var r,s={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)Bd.call(t,r)&&!Hd.hasOwnProperty(r)&&(s[r]=t[r]);var a=arguments.length-2;if(a===1)s.children=n;else if(1>>1,U=x[D];if(0Web source attribution {data.web_available ? "" : "(not configured)"}
+Web source attribution · {data.web_provider ? data.web_provider.charAt(0).toUpperCase() + data.web_provider.slice(1) : "unavailable"}
{data.web_available ? ( (data.web_matches?.length || 0) > 0 ? (✓ No matching sources found on the open web.
+✓ Checked the live web ({data.web_provider || "search"}) — no close matches for the flagged text.
) ) : ({data.web_summary}
@@ -418,6 +418,7 @@ function buildReplayProvenance(record) { web_matches: details.web_matches || [], web_summary: details.web_summary || "", web_available: details.web_available === true, + web_provider: details.web_provider || "", }; }