feat: 更新首页信息处理模型,支持内存缓存与过期机制;添加全量目录同步脚本
This commit is contained in:
@@ -35,6 +35,7 @@ async def setup(app, monkeypatch):
|
||||
async with app.state.sessions.begin() as db:
|
||||
account = await db.get(Account, 1)
|
||||
account.connection_status, account.wq_user_id = "connected", "TEST_USER"
|
||||
monkeypatch.setattr(home, "now", lambda: datetime.fromisoformat("2026-09-12T12:00:00+00:00"))
|
||||
calls, state = [], {"rank": 25, "fail": set(), "agreement": "Participants must use GLOBAL region. Delay must be 1."}
|
||||
|
||||
async def get(path, params=None, headers=None):
|
||||
@@ -43,7 +44,7 @@ async def setup(app, monkeypatch):
|
||||
raise WqError("资源暂不可用")
|
||||
if path == "/users/self/messages":
|
||||
offset = params["offset"]
|
||||
return page([{"id": f"msg{offset}", "title": "Update", "description": "<p>News</p><script>secret()</script><a href='javascript:alert(1)'>bad</a>", "dateCreated": "2026-09-11T12:00:00-04:00"}],
|
||||
return page([{"id": f"msg{i}", "title": "Update", "description": "<p>News</p><script>secret()</script><a href='javascript:alert(1)'>bad</a>", "dateCreated": "2026-09-11T12:00:00-04:00"} for i in (range(10) if offset == 0 else [10])],
|
||||
"https://api.worldquantbrain.com/users/self/messages?offset=1" if offset == 0 else None)
|
||||
if path == "/consultant/boards/leader":
|
||||
assert params == {"user": "TEST_USER"}
|
||||
@@ -64,12 +65,12 @@ async def setup(app, monkeypatch):
|
||||
async def test_cache_pagination_failure_and_account_isolation(app, logged_in, monkeypatch):
|
||||
calls, state = await setup(app, monkeypatch)
|
||||
first = (await logged_in.get(PREFIX + "/messages")).json()
|
||||
assert first["content"]["next_offset"] == 1
|
||||
assert first["content"]["next_offset"] == 10
|
||||
assert "secret()" not in str(first) and "javascript:" not in str(first)
|
||||
assert first["analysis"] is None and not first["can_generate"]
|
||||
assert (await logged_in.get(PREFIX + "/messages")).json() == first and len(calls) == 1
|
||||
second = (await logged_in.get(PREFIX + "/messages?offset=1")).json()
|
||||
assert second["content"]["items"][0]["id"] == "msg1"
|
||||
assert (await logged_in.get(PREFIX + "/messages")).json() == first and len(calls) == 2
|
||||
second = (await logged_in.get(PREFIX + "/messages?offset=10")).json()
|
||||
assert second["content"]["items"][0]["id"] == "msg10"
|
||||
state["fail"].add("/users/self/messages")
|
||||
failed = (await logged_in.post(PREFIX + "/messages/refresh")).json()
|
||||
assert failed["stale"] and failed["error"]
|
||||
@@ -193,7 +194,7 @@ async def test_failed_grounding_preserves_prior_analysis(app, logged_in, monkeyp
|
||||
async def test_concurrent_first_reads_are_coalesced(app, logged_in, monkeypatch):
|
||||
calls, _ = await setup(app, monkeypatch)
|
||||
results = await asyncio.gather(*[logged_in.get(PREFIX + "/messages") for _ in range(4)])
|
||||
assert all(r.status_code == 200 for r in results) and len(calls) == 1
|
||||
assert all(r.status_code == 200 for r in results) and len(calls) == 2
|
||||
|
||||
|
||||
async def test_event_sources_include_competitions_and_current_day(app, logged_in, monkeypatch):
|
||||
@@ -298,3 +299,93 @@ async def test_shared_connection_change_invalidates_ai(app, logged_in, monkeypat
|
||||
setattr(await db.get(AISettings, 1), field, value)
|
||||
assert (await logged_in.get(PREFIX + '/competition' + QUERY)).json()['analysis']['outdated']
|
||||
assert calls == ['basic-model']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('at,expected', [
|
||||
('2026-03-31T12:00:00-04:00', '2026-02-28T12:00:00-05:00'),
|
||||
('2024-03-31T12:00:00-04:00', '2024-02-29T12:00:00-05:00'),
|
||||
('2026-01-31T12:00:00-05:00', '2025-12-31T12:00:00-05:00'),
|
||||
])
|
||||
def test_recent_month_uses_eastern_calendar_month(monkeypatch, at, expected):
|
||||
monkeypatch.setattr(home, 'now', lambda: datetime.fromisoformat(at))
|
||||
assert home.month_start().isoformat() == expected
|
||||
|
||||
|
||||
async def test_recent_messages_scan_pages_filter_unknown_future_and_boundary(monkeypatch):
|
||||
monkeypatch.setattr(home, 'now', lambda: datetime.fromisoformat('2026-03-31T12:00:00-04:00'))
|
||||
calls = []
|
||||
class Client:
|
||||
async def get(self, path, params):
|
||||
calls.append(params)
|
||||
if params['offset'] == 0:
|
||||
return page([{'id': 'old', 'dateCreated': '2026-02-28T11:59:59-05:00'},
|
||||
{'id': 'unknown'}, {'id': 'future', 'dateCreated': '2026-04-01T00:00:00Z'}],
|
||||
'https://api.worldquantbrain.com/users/self/messages?offset=3&limit=100')
|
||||
return page([{'id': 'boundary', 'dateCreated': '2026-02-28T12:00:00-05:00'},
|
||||
{'id': 'today', 'dateCreated': '2026-03-31T12:00:00-04:00'}])
|
||||
data = await home.source(Client(), 'messages', 'TEST_USER', 0, '')
|
||||
assert [item['id'] for item in data['items']] == ['today', 'boundary']
|
||||
assert calls == [{'offset': 0, 'limit': 100}, {'offset': 3, 'limit': 100}]
|
||||
|
||||
|
||||
async def test_messages_and_ai_never_persist_and_expire_without_auto_generation(app, logged_in, monkeypatch):
|
||||
platform_calls, _ = await setup(app, monkeypatch)
|
||||
await model_setup(app)
|
||||
model_calls = []
|
||||
def respond(messages, info):
|
||||
return ModelResponse(parts=[ToolCallPart(info.output_tools[0].name, {'items': [], 'suggestions': []})])
|
||||
@asynccontextmanager
|
||||
async def factory(config, settings):
|
||||
model_calls.append(config.model)
|
||||
yield FunctionModel(respond)
|
||||
app.state.ai.model_factory = factory
|
||||
# Legacy rows must never be used; migration will remove them on deployment.
|
||||
async with app.state.sessions.begin() as db:
|
||||
db.add(HomeInformation(user_id='TEST_USER', resource='messages:0', content={'items': [{'title': 'legacy'}]}))
|
||||
first = (await logged_in.get(PREFIX + '/messages')).json()
|
||||
assert first['ephemeral'] and first['content']['total'] == 11
|
||||
generated = await logged_in.post(PREFIX + '/messages/generate')
|
||||
assert generated.status_code == 200, generated.text
|
||||
assert generated.json()['analysis']['model'] == 'basic-model'
|
||||
await logged_in.get(PREFIX + '/messages')
|
||||
await logged_in.get(PREFIX + '/messages?offset=10')
|
||||
assert len(platform_calls) == 2 and model_calls == ['basic-model']
|
||||
async with app.state.sessions() as db:
|
||||
saved = (await db.scalars(sa.select(HomeInformation))).all()
|
||||
assert len(saved) == 1 and saved[0].content == {'items': [{'title': 'legacy'}]}
|
||||
assert saved[0].analysis is None
|
||||
monkeypatch.setattr(home, 'now', lambda: datetime.fromisoformat('2026-09-12T12:15:00+00:00'))
|
||||
assert (await logged_in.post(PREFIX + '/messages/generate')).status_code == 409
|
||||
reloaded = (await logged_in.get(PREFIX + '/messages')).json()
|
||||
assert reloaded['analysis'] is None and len(platform_calls) == 4
|
||||
assert model_calls == ['basic-model']
|
||||
|
||||
|
||||
async def test_message_window_rechecks_memory_and_removes_expired_interpretation(app, logged_in, monkeypatch):
|
||||
await setup(app, monkeypatch)
|
||||
monkeypatch.setattr(home, 'now', lambda: datetime.fromisoformat('2026-09-12T12:00:00+00:00'))
|
||||
async def upstream(path, params=None):
|
||||
return page([{'id': 'boundary', 'dateCreated': '2026-08-12T08:00:00-04:00'}])
|
||||
monkeypatch.setattr(app.state.runner.client, 'get', upstream)
|
||||
assert (await logged_in.get(PREFIX + '/messages')).json()['content']['total'] == 1
|
||||
app.state.home_message_cache['TEST_USER'].analyses[0] = {'old': 'interpretation'}
|
||||
monkeypatch.setattr(home, 'now', lambda: datetime.fromisoformat('2026-09-12T12:00:01+00:00'))
|
||||
result = (await logged_in.get(PREFIX + '/messages')).json()
|
||||
assert result['content']['total'] == 0 and result['analysis'] is None
|
||||
|
||||
|
||||
def test_message_cleanup_migration_only_removes_message_cache(tmp_path):
|
||||
path = Path(__file__).parents[1] / 'migrations/versions/0017_remove_message_cache.py'
|
||||
spec = importlib.util.spec_from_file_location('message_cleanup', path)
|
||||
migration = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(migration)
|
||||
engine = sa.create_engine(f'sqlite:///{tmp_path}/cleanup.db')
|
||||
with engine.begin() as conn:
|
||||
conn.exec_driver_sql('CREATE TABLE home_information (user_id TEXT, resource TEXT, content TEXT, analysis TEXT)')
|
||||
for user, resource in [('A', 'messages:0'), ('B', 'messages:10'), ('A', 'events:'), ('A', 'competition:ARC2026')]:
|
||||
conn.exec_driver_sql('INSERT INTO home_information VALUES (?, ?, ?, ?)', (user, resource, 'source', 'ai'))
|
||||
with Operations.context(MigrationContext.configure(conn)):
|
||||
migration.upgrade()
|
||||
migration.upgrade()
|
||||
assert conn.exec_driver_sql('SELECT resource FROM home_information ORDER BY resource').scalars().all() == ['competition:ARC2026', 'events:']
|
||||
engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user