File size: 13,264 Bytes
1642ffb
 
 
 
cc151a4
 
1642ffb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79e11ca
 
1642ffb
 
 
 
 
 
 
 
79e11ca
 
 
 
 
1642ffb
 
 
 
 
 
 
79e11ca
 
 
 
 
 
1642ffb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc151a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1642ffb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import asyncio
import base64
import io
import struct
import time
from types import SimpleNamespace

import httpx
import pytest
from PIL import Image
from pydantic import SecretStr, ValidationError

from backend.main import create_app
from backend.schemas import AVChunk
from backend.settings import Settings, validate_service_url


def start_body(duration=1000):
    return {"holdings": [{"id": "xom", "name": "Exxon Mobil Corporation", "ticker": "XOM",
                          "exchange": "NYSE", "currency": "USD", "asset_type": "equity", "weight": 100}],
            "objective": "Research how the reported oil supply conditions might affect this holding.",
            "duration_ms": duration, "source_name": "authorized-fixture.mp4", "authorized": True}


def media_chunk(sequence=0, count=16000, epoch=0):
    image = io.BytesIO()
    Image.new("RGB", (64, 36), (100 + sequence % 100, 100, 100)).save(image, "JPEG")
    # Non-silent alternating signal goes through base64/float32 validation.
    audio = struct.pack("<" + "f" * count, *[0.25 if i % 2 else -0.25 for i in range(count)])
    begin = sequence * 1000
    return {"epoch": epoch, "sequence": sequence, "source_start_ms": begin,
            "source_end_ms": begin + count / 16, "sample_rate": 16000, "sample_count": count,
            "audio_b64": base64.b64encode(audio).decode(), "final_chunk": count < 16000,
            "capture_time_ms": 1750000000000.125,
            "frames": [{"source_time_ms": begin + 0.0625, "jpeg_b64": base64.b64encode(image.getvalue()).decode()}]}


@pytest.mark.asyncio
async def test_connected_api_upload_partial_completion_and_retained_results():
    app = create_app(Settings(mock_latency_seconds=0))
    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=("127.0.0.1", 44)), base_url="http://test") as client:
            result = await client.post("/api/sessions", json=start_body(1062.5))
            assert result.status_code == 200, result.text
            credentials = result.json()
            base = "/api/sessions/" + credentials["session_id"]
            headers = {"X-Session-Token": credentials["session_token"]}
            assert (await client.get(base)).status_code == 404
            assert (await client.get(base + '/timings')).status_code == 404
            assert (await client.get(base + '?after_revision=0&wait_seconds=20')).status_code == 404
            for payload in [media_chunk(), media_chunk(1, 1000)]:
                response = await client.post(base + "/chunks", headers=headers, json=payload)
                assert response.status_code == 200, response.text
                repeated = await client.post(base + "/chunks", headers=headers, json=payload)
                assert repeated.json()["duplicate"]
            assert (await client.post(base + "/complete", headers=headers)).status_code == 200
            session = app.state.sessions[credentials["session_id"]]
            await session.completion_task
            live_response = await client.get(base + f'?after_revision={session.revision}&wait_seconds=20', headers=headers)
            assert live_response.status_code == 200  # Terminal state returns without waiting.
            assert live_response.headers['cache-control'] == 'no-store'
            assert credentials['session_token'] not in live_response.text
            assert (await client.get(base + '?after_revision=0&wait_seconds=99', headers=headers)).status_code == 422
            snapshot = (await client.get(base, headers=headers)).json()
            assert snapshot["state"] == "completed"
            assert snapshot["coverage"]["processed_chunks"] == 2
            assert snapshot["coverage"]["processed_audio_ms"] == 1062.5
            assert snapshot["coverage"]["complete"]
            assert snapshot["evidence"][0]["source_start_ms"] == 0
            assert snapshot["assessments"][0]["implications"][0]["holding_id"] == "xom"
            report = await client.get(base + '/timings', headers=headers)
            assert report.status_code == 200 and report.headers['cache-control'] == 'no-store'
            assert len(report.json()['units']) == 2
            assert report.json()['summary']['contiguous_processed_source_ms'] == 1062.5
            for private in (credentials['session_token'], 'Exxon', 'authorized-fixture.mp4', 'audio_b64', 'jpeg_b64'):
                assert private not in report.text
            assert "discussion" not in snapshot
            assert snapshot["coverage"]["final_summary_requests"] == 0
            assert snapshot["brief"]["simulated"]
            assert snapshot["brief"]["complete"]
            assert credentials["session_token"] not in str(snapshot)
            assert (await client.post(base + "/stop", headers=headers)).json()["brief"] == snapshot["brief"]
            diagnostic = {"epoch": 1, "code": "audio_capture_gap", "message": "fixture", "source_start_ms": 0, "source_end_ms": 8}
            assert (await client.post(base + "/diagnostics", headers=headers, json=diagnostic)).status_code == 409
            diagnostic["epoch"] = 0
            assert (await client.post(base + "/diagnostics", headers=headers, json=diagnostic)).status_code == 200
            updated = (await client.get(base, headers=headers)).json()
            assert not updated["brief"]["complete"]
            assert updated["coverage"]["gaps"][-1]["source_end_ms"] == 8
            assert updated["coverage"]["final_summary_requests"] == 0



@pytest.mark.asyncio
async def test_auth_origin_url_and_body_safeguards():
    cfg = Settings(app_access_password=SecretStr("test-workspace-password-only"))
    app = create_app(cfg)
    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
            assert (await client.post("/api/sessions", json=start_body())).status_code == 401
            response = await client.post("/api/login", json={"password": "test-workspace-password-only"})
            assert response.status_code == 200, response.text
            assert "HttpOnly" in response.headers["set-cookie"]
            assert "test-workspace-password-only" not in response.text
            forbidden = await client.post("/api/sessions", json=start_body(), headers={"origin": "https://hostile.example"})
            assert forbidden.status_code == 403
            too_large = await client.post("/api/sessions", content=b"x" * 1_650_001)
            assert too_large.status_code == 413
            result = await client.post("/api/sessions", json=start_body())
            assert result.status_code == 200, result.text
    for url in ["http://169.254.169.254", "https://example.com", "https://foo.hf.space/path", "https://x:y@foo.hf.space", "https://foo.hf.space?token=bad"]:
        with pytest.raises(ValueError):
            validate_service_url(url, True)
    assert validate_service_url("https://test.hf.space") == "https://test.hf.space"


@pytest.mark.asyncio
async def test_workspace_login_starts_without_cookies_and_rejects_expired_access(monkeypatch):
    app = create_app(Settings(app_access_password=SecretStr("local-auth-fixture-only"),
                              app_origin="https://workspace.test", mock_latency_seconds=0))
    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="https://workspace.test") as client:
            assert (await client.get("/api/config")).json()["requires_login"]
            rejected = await client.post("/api/login", json={"password": "wrong-fixture"})
            assert rejected.status_code == 401 and "workspace_token" not in rejected.json()
            login = await client.post("/api/login", json={"password": "local-auth-fixture-only"})
            assert login.status_code == 200 and login.headers["cache-control"] == "no-store"
            assert "local-auth-fixture-only" not in login.text
            token = login.json()["workspace_token"]
            assert len(token) == 43
            assert not (await client.get("/api/config")).json()["requires_login"]
            # Reproduce a cross-site Space iframe that cannot retain cookies.
            client.cookies.clear()
            assert (await client.get("/api/config")).json()["requires_login"]
            assert (await client.post("/api/sessions", json=start_body())).status_code == 401
            assert (await client.post("/api/sessions", json=start_body(), headers={"X-Workspace-Token": "invalid"})).status_code == 401
            headers = {"X-Workspace-Token": token, "Origin": "https://workspace.test"}
            config = await client.get("/api/config", headers=headers)
            assert not config.json()["requires_login"] and token not in config.text
            assert (await client.post("/api/sessions", json=start_body(), headers={**headers, "Origin": "https://hostile.example"})).status_code == 403
            result = await client.post("/api/sessions", json=start_body(), headers=headers)
            assert result.status_code == 200 and token not in result.text
            session = result.json()
            assert (await client.post(f'/api/sessions/{session["session_id"]}/stop', headers={"X-Session-Token": session["session_token"]})).status_code == 200
            expired_time = time.monotonic() + 8 * 3600 + 1
            monkeypatch.setattr("backend.main.time", SimpleNamespace(monotonic=lambda: expired_time))
            assert (await client.get("/api/config", headers=headers)).json()["requires_login"]
            assert (await client.post("/api/sessions", json=start_body(), headers=headers)).status_code == 401


@pytest.mark.parametrize("update", [
    {"audio_b64": "invalid***"}, {"sample_count": 15999}, {"sample_rate": 48000},
    {"source_start_ms": 300}, {"source_end_ms": float("nan")},
    {"frames": [{"source_time_ms": 9000, "jpeg_b64": "/9j/2Q=="}]},
])
def test_invalid_audio_and_timestamps_rejected_before_model(update):
    with pytest.raises(ValidationError):
        AVChunk.model_validate({**media_chunk(), **update})


def test_600_second_budget_is_derived_from_native_cadence():
    settings = Settings()
    assert settings.chunk_budget(600000) >= 602
    assert settings.chunk_budget(600001) > settings.chunk_budget(600000)
    assert settings.max_duration_ms >= 600000
    assert settings.session_wall_timeout_seconds > 600 + settings.completion_timeout_seconds


def test_invalid_runtime_configuration_never_prints_secret_input():
    secret = "runtime-value-that-must-not-appear-in-error"
    with pytest.raises(ValidationError) as caught:
        Settings(app_mode="real", hf_token=secret, minicpm_service_url="https://invalid.example")
    assert secret not in str(caught.value)


@pytest.mark.asyncio
async def test_short_audio_tail_with_retained_visual_context_is_not_false_gap():
    from backend.schemas import StartRequest
    from backend.session import Session
    session = Session(StartRequest.model_validate(start_body(1037)), Settings(mock_latency_seconds=0))
    await session.start()
    await session.accept(AVChunk.model_validate(media_chunk()))
    tail = media_chunk(1, 592)
    tail["frames"] = []
    await session.accept(AVChunk.model_validate(tail))
    await session.complete()
    await session.completion_task
    assert session.coverage["complete"]
    assert session.coverage["processed_audio_ms"] == 1037
    assert "final_visual_hold" in session.diagnostics


@pytest.mark.asyncio
async def test_start_disconnect_cancels_native_preparation(monkeypatch):
    from backend.session import Session
    entered, cancelled, stopped = asyncio.Event(), asyncio.Event(), asyncio.Event()
    original_stop = Session.stop
    async def blocked_start(self):
        entered.set()
        try:
            await asyncio.sleep(30)
        except asyncio.CancelledError:
            cancelled.set()
            raise
    async def stop(self, reason="Manual Stop"):
        stopped.set()
        await original_stop(self, reason)
    monkeypatch.setattr(Session, "start", blocked_start)
    monkeypatch.setattr(Session, "stop", stop)
    app = create_app(Settings())
    import json
    body = json.dumps(start_body()).encode()
    sent = False
    async def receive():
        nonlocal sent
        if not sent:
            sent = True
            return {"type": "http.request", "body": body, "more_body": False}
        await entered.wait()
        return {"type": "http.disconnect"}
    messages = []
    async def send(message):
        messages.append(message)
    scope = {"type": "http", "asgi": {"version": "3.0"}, "http_version": "1.1", "method": "POST",
             "scheme": "http", "path": "/api/sessions", "raw_path": b"/api/sessions", "query_string": b"",
             "headers": [(b"content-type", b"application/json"), (b"content-length", str(len(body)).encode())],
             "client": ("127.0.0.1", 123), "server": ("127.0.0.1", 8000), "root_path": ""}
    async with app.router.lifespan_context(app):
        await asyncio.wait_for(app(scope, receive, send), 3)
        assert cancelled.is_set() and stopped.is_set()
        assert all(s.state == "stopped" for s in app.state.sessions.values())