feat(push,terminal): 推送搜索历史5条与终端中间搜索
推送页独立 combobox 历史(MySQL push_search,上限5);终端空态搜索回中间卡片,右侧栏仅列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# 审计 — 推送搜索历史 + 终端搜索回中间
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-12-push-server-search-history.md`
|
||||
- `docs/changelog/2026-06-12-terminal-search-center-empty-state.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/server_search_history.py` | push 上下文,上限 5 |
|
||||
| `server/api/servers.py` | push-search-history API |
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | scope 参数 |
|
||||
| `frontend/src/composables/push/usePushServers.ts` | 推送搜索历史 |
|
||||
| `frontend/src/composables/push/usePushPage.ts` | 加载历史 |
|
||||
| `frontend/src/components/push/PushServerGrid.vue` | combobox |
|
||||
| `frontend/src/pages/PushPage.vue` | 绑定历史 |
|
||||
| `frontend/src/pages/TerminalPage.vue` | 中间空态搜索 |
|
||||
| `frontend/src/components/terminal/TerminalArea.vue` | empty-picker 插槽 |
|
||||
| `frontend/src/components/terminal/TerminalServerPicker.vue` | 侧栏无搜索 |
|
||||
| `tests/test_server_search_history.py` | push 上限与 API |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — 按 admin 隔离偏好 |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD | N/A — 仅 UI 偏好 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 复用 admin_ui_preferences |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [x] pytest test_server_search_history
|
||||
- [ ] 生产 `#/push` / `#/terminal` 验证
|
||||
|
||||
## 验证
|
||||
|
||||
推送:combobox 最近 5 条。终端:无会话时中间搜索,右侧仅列表。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 推送页服务器搜索历史(5 条)
|
||||
|
||||
**日期**:2026-06-12
|
||||
|
||||
## 动机
|
||||
|
||||
推送页搜索服务器需记住最近查询,便于重复筛选;与服务器页历史独立,最多保留 5 条。
|
||||
|
||||
## 变更
|
||||
|
||||
- **后端**:`GET/PUT /api/servers/push-search-history`,`admin_ui_preferences` 上下文 `push_search`,上限 5 条
|
||||
- **前端**:搜索框改为 combobox;回车或选历史项写入;与服务器页历史互不影响
|
||||
- **复用**:`useServerSearchHistory('push')` 与现有 MySQL 偏好存储一致
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/utils/server_search_history.py`
|
||||
- `server/api/servers.py`
|
||||
- `frontend/src/composables/useServerSearchHistory.ts`
|
||||
- `frontend/src/composables/push/usePushServers.ts`
|
||||
- `frontend/src/composables/push/usePushPage.ts`
|
||||
- `frontend/src/components/push/PushServerGrid.vue`
|
||||
- `frontend/src/pages/PushPage.vue`
|
||||
- `tests/test_server_search_history.py`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
cd frontend && npm run type-check
|
||||
pytest tests/test_server_search_history.py -q
|
||||
```
|
||||
|
||||
推送页:输入关键词回车 → 下拉可见最近 5 条;换浏览器登录同一账号仍可恢复。
|
||||
|
||||
## 迁移
|
||||
|
||||
无新表;首次写入自动创建 `push_search` 偏好记录。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 终端搜索框回到中间空态
|
||||
|
||||
**日期**:2026-06-12
|
||||
|
||||
## 动机
|
||||
|
||||
终端页右侧栏保留服务器列表,但搜索框应仍在中间空态卡片(与改版前一致),不在右侧栏重复。
|
||||
|
||||
## 变更
|
||||
|
||||
- 恢复 `TerminalArea` 的 `#empty-picker` 插槽与中间 inline 选服卡片(含搜索 + 历史)
|
||||
- 右侧 `sidebar` 模式仅展示服务器列表,移除顶部 combobox
|
||||
- 空态文案恢复为「选择一台服务器开始会话…」
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/pages/TerminalPage.vue`
|
||||
- `frontend/src/components/terminal/TerminalArea.vue`
|
||||
- `frontend/src/components/terminal/TerminalServerPicker.vue`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
cd frontend && npm run type-check
|
||||
```
|
||||
|
||||
无会话时:中间有搜索框;右侧仅列表。中间搜索仍过滤两侧列表。
|
||||
|
||||
## 迁移
|
||||
|
||||
无。仅前端构建部署。
|
||||
@@ -9,16 +9,21 @@
|
||||
class="mr-2"
|
||||
@update:model-value="$emit('toggle-all')"
|
||||
/>
|
||||
<v-text-field
|
||||
<v-combobox
|
||||
:model-value="serverSearch"
|
||||
:items="searchHistory"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="搜索服务器..."
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
rounded
|
||||
clearable
|
||||
:auto-select-first="false"
|
||||
placeholder="输入后回车,或从历史选择"
|
||||
style="max-width: 240px"
|
||||
@update:model-value="$emit('update:serverSearch', $event)"
|
||||
@update:model-value="onSearchUpdate"
|
||||
@keydown.enter.prevent="$emit('search-commit')"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
@@ -177,6 +182,7 @@ import type { PushServerItem } from '@/composables/push/types'
|
||||
defineProps<{
|
||||
selectAll: boolean
|
||||
serverSearch: string
|
||||
searchHistory: string[]
|
||||
serversLoading: boolean
|
||||
serversByCategory: { category: string; label: string; servers: PushServerItem[] }[]
|
||||
selectedIds: Set<number>
|
||||
@@ -184,8 +190,9 @@ defineProps<{
|
||||
isCategoryAllSelected: (category: string) => boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:serverSearch': [value: string]
|
||||
const emit = defineEmits<{
|
||||
'update:serverSearch': [value: string | null]
|
||||
'search-commit': []
|
||||
'toggle-all': []
|
||||
refresh: []
|
||||
'toggle-category': [category: string]
|
||||
@@ -194,6 +201,10 @@ defineEmits<{
|
||||
|
||||
const { viewMode } = usePushServerViewMode()
|
||||
|
||||
function onSearchUpdate(val: string | null) {
|
||||
emit('update:serverSearch', val)
|
||||
}
|
||||
|
||||
function serverLabelTitle(s: PushServerItem): string {
|
||||
const parts = [s.name]
|
||||
if (s.domain?.trim()) parts.push(s.domain.trim())
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
>
|
||||
<v-icon size="56" color="primary" class="mb-3">mdi-console-network</v-icon>
|
||||
<div class="text-h6 mb-1 text-center">WebSSH 终端</div>
|
||||
<div class="text-body-2 text-medium-emphasis text-center">
|
||||
从右侧列表选择服务器,或点击上方 + 新建标签
|
||||
<div class="text-body-2 text-medium-emphasis mb-4 text-center">
|
||||
选择一台服务器开始会话,或从服务器页点击「终端」
|
||||
</div>
|
||||
<slot name="empty-picker" />
|
||||
</div>
|
||||
|
||||
<v-overlay
|
||||
|
||||
@@ -4,25 +4,7 @@
|
||||
服务器
|
||||
<span class="text-caption text-medium-emphasis ml-1">({{ servers.length }})</span>
|
||||
</div>
|
||||
<div class="px-3 py-2 flex-shrink-0">
|
||||
<v-combobox
|
||||
:model-value="search"
|
||||
:items="searchHistory"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="搜索名称/域名…"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
:auto-select-first="false"
|
||||
@update:model-value="$emit('update:search', $event)"
|
||||
@keydown.enter.prevent="$emit('search-commit')"
|
||||
/>
|
||||
<div v-if="searchHistory.length" class="text-caption text-medium-emphasis mt-1">
|
||||
最近 {{ searchHistory.length }} 条与服务器页同步
|
||||
</div>
|
||||
</div>
|
||||
<v-skeleton-loader v-if="loading" type="list-item@8" class="px-2" />
|
||||
<v-skeleton-loader v-if="loading" type="list-item@8" class="px-2 pt-2" />
|
||||
<v-list v-else density="compact" nav class="terminal-server-rail__list flex-grow-1 py-0">
|
||||
<v-list-item
|
||||
v-for="s in servers"
|
||||
@@ -81,12 +63,12 @@ import type { TerminalServerItem } from '@/composables/terminal/types'
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
servers: TerminalServerItem[]
|
||||
search: string
|
||||
search?: string
|
||||
searchHistory?: string[]
|
||||
loading?: boolean
|
||||
mode?: 'inline' | 'dialog' | 'sidebar'
|
||||
}>(),
|
||||
{ mode: 'inline', loading: false, searchHistory: () => [] },
|
||||
{ mode: 'inline', loading: false, search: '', searchHistory: () => [] },
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
|
||||
@@ -34,6 +34,7 @@ export function usePushPage() {
|
||||
|
||||
onMounted(() => {
|
||||
refreshPush(false)
|
||||
void servers.loadSearchHistory()
|
||||
servers.startAutoRefresh(() => progress.pushing.value)
|
||||
pushReady = true
|
||||
})
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import { categoryDisplayLabel } from '@/composables/useServerCategories'
|
||||
import { useServerSearchHistory } from '@/composables/useServerSearchHistory'
|
||||
import { fetchPagePerPage } from '@/utils/paginatedFetch'
|
||||
import type { PushServerItem } from './types'
|
||||
|
||||
const PUSH_SEARCH_HISTORY = 5
|
||||
|
||||
export function usePushServers(pushStatus: Ref<Record<number, string>>) {
|
||||
const servers = ref<PushServerItem[]>([])
|
||||
const serversLoading = ref(false)
|
||||
const serverSearch = ref('')
|
||||
const selectedIds = ref<Set<number>>(new Set())
|
||||
|
||||
const { items: pushSearchHistory, loadHistoryOnly, record: recordPushSearch } =
|
||||
useServerSearchHistory('push')
|
||||
|
||||
const serverSearchHistory = computed(() =>
|
||||
pushSearchHistory.value.slice(0, PUSH_SEARCH_HISTORY),
|
||||
)
|
||||
|
||||
function commitServerSearch() {
|
||||
void recordPushSearch(serverSearch.value)
|
||||
}
|
||||
|
||||
function onServerSearchUpdate(val: string | null) {
|
||||
serverSearch.value = val ?? ''
|
||||
if (val == null || val === '') {
|
||||
void recordPushSearch('')
|
||||
return
|
||||
}
|
||||
const picked = String(val).trim()
|
||||
if (pushSearchHistory.value.includes(picked)) {
|
||||
serverSearch.value = picked
|
||||
void recordPushSearch(picked)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSearchHistory() {
|
||||
await loadHistoryOnly()
|
||||
}
|
||||
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function loadServers(silent = false) {
|
||||
@@ -113,12 +144,16 @@ export function usePushServers(pushStatus: Ref<Record<number, string>>) {
|
||||
servers,
|
||||
serversLoading,
|
||||
serverSearch,
|
||||
serverSearchHistory,
|
||||
selectedIds,
|
||||
pushStatus,
|
||||
filteredServers,
|
||||
selectAll,
|
||||
serversByCategory,
|
||||
loadServers,
|
||||
loadSearchHistory,
|
||||
commitServerSearch,
|
||||
onServerSearchUpdate,
|
||||
startAutoRefresh,
|
||||
stopAutoRefresh,
|
||||
isCategoryAllSelected,
|
||||
|
||||
@@ -33,24 +33,34 @@ function clearLegacyLocal() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Servers page search history — persisted per admin in MySQL. */
|
||||
export function useServerSearchHistory() {
|
||||
/** Servers / push page search history — persisted per admin in MySQL. */
|
||||
export type ServerSearchHistoryScope = 'servers' | 'push'
|
||||
|
||||
const HISTORY_ENDPOINT: Record<ServerSearchHistoryScope, string> = {
|
||||
servers: '/servers/search-history',
|
||||
push: '/servers/push-search-history',
|
||||
}
|
||||
|
||||
/** Per-admin search history in MySQL (servers or push scope). */
|
||||
export function useServerSearchHistory(scope: ServerSearchHistoryScope = 'servers') {
|
||||
const items = ref<string[]>([])
|
||||
const bootstrapped = ref(false)
|
||||
const endpoint = HISTORY_ENDPOINT[scope]
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const res = await http.get<ServerSearchHistoryResponse>('/servers/search-history')
|
||||
const res = await http.get<ServerSearchHistoryResponse>(endpoint)
|
||||
items.value = res.history || []
|
||||
}
|
||||
|
||||
async function migrateLegacyIfEmpty(): Promise<boolean> {
|
||||
if (scope !== 'servers') return false
|
||||
const legacy = readLegacyLocal()
|
||||
if (!legacy) return false
|
||||
if (items.value.length > 0) {
|
||||
clearLegacyLocal()
|
||||
return false
|
||||
}
|
||||
await http.put<ServerSearchHistoryResponse>('/servers/search-history', {
|
||||
await http.put<ServerSearchHistoryResponse>(endpoint, {
|
||||
history: legacy.history,
|
||||
last: legacy.last || legacy.history[0] || '',
|
||||
})
|
||||
@@ -76,7 +86,7 @@ export function useServerSearchHistory() {
|
||||
async function record(query: string | null | undefined) {
|
||||
const normalized = String(query ?? '').trim()
|
||||
try {
|
||||
const res = await http.put<ServerSearchHistoryResponse>('/servers/search-history', {
|
||||
const res = await http.put<ServerSearchHistoryResponse>(endpoint, {
|
||||
query: normalized,
|
||||
})
|
||||
items.value = res.history || []
|
||||
|
||||
@@ -62,12 +62,15 @@
|
||||
|
||||
<PushServerGrid
|
||||
:select-all="servers.selectAll"
|
||||
v-model:server-search="servers.serverSearch"
|
||||
:server-search="servers.serverSearch"
|
||||
:search-history="servers.serverSearchHistory"
|
||||
:servers-loading="servers.serversLoading"
|
||||
:servers-by-category="servers.serversByCategory"
|
||||
:selected-ids="servers.selectedIds"
|
||||
:push-status="servers.pushStatus"
|
||||
:is-category-all-selected="servers.isCategoryAllSelected"
|
||||
@update:server-search="servers.onServerSearchUpdate"
|
||||
@search-commit="servers.commitServerSearch"
|
||||
@toggle-all="servers.toggleAll"
|
||||
@refresh="servers.loadServers"
|
||||
@toggle-category="servers.toggleCategory"
|
||||
|
||||
@@ -67,7 +67,20 @@
|
||||
@pane-ref="setTermRef"
|
||||
@reconnect="reconnect"
|
||||
@go-servers="router.push('/servers')"
|
||||
/>
|
||||
>
|
||||
<template #empty-picker>
|
||||
<TerminalServerPicker
|
||||
:servers="filteredServers"
|
||||
:search="serverSearch"
|
||||
:search-history="terminalSearchHistory"
|
||||
:loading="serversLoading"
|
||||
mode="inline"
|
||||
@update:search="onServerSearchUpdate"
|
||||
@search-commit="commitServerSearch"
|
||||
@select="(id) => newSession(id)"
|
||||
/>
|
||||
</template>
|
||||
</TerminalArea>
|
||||
|
||||
<TerminalQuickBar
|
||||
:commands="quickCmds"
|
||||
@@ -88,11 +101,7 @@
|
||||
<TerminalServerPicker
|
||||
mode="sidebar"
|
||||
:servers="filteredServers"
|
||||
:search="serverSearch"
|
||||
:search-history="terminalSearchHistory"
|
||||
:loading="serversLoading"
|
||||
@update:search="onServerSearchUpdate"
|
||||
@search-commit="commitServerSearch"
|
||||
@select="(id) => newSession(id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -344,6 +344,61 @@ async def update_server_search_history(
|
||||
return state
|
||||
|
||||
|
||||
@router.get("/push-search-history", response_model=dict)
|
||||
async def get_push_search_history(
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return push page server search history (max 5) for the current admin."""
|
||||
from server.infrastructure.database.admin_ui_preference_repo import AdminUiPreferenceRepositoryImpl
|
||||
from server.utils.server_search_history import (
|
||||
MAX_PUSH_SEARCH_HISTORY,
|
||||
PUSH_SEARCH_CONTEXT,
|
||||
parse_search_state,
|
||||
)
|
||||
|
||||
repo = AdminUiPreferenceRepositoryImpl(db)
|
||||
raw = await repo.get(admin.id, PUSH_SEARCH_CONTEXT)
|
||||
return parse_search_state(raw, max_history=MAX_PUSH_SEARCH_HISTORY)
|
||||
|
||||
|
||||
@router.put("/push-search-history", response_model=dict)
|
||||
async def update_push_search_history(
|
||||
payload: ServerSearchHistoryUpdate,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Record a push page search query or replace full history (max 5)."""
|
||||
from server.infrastructure.database.admin_ui_preference_repo import AdminUiPreferenceRepositoryImpl
|
||||
from server.utils.server_search_history import (
|
||||
MAX_PUSH_SEARCH_HISTORY,
|
||||
PUSH_SEARCH_CONTEXT,
|
||||
apply_search_record,
|
||||
apply_search_replace,
|
||||
parse_search_state,
|
||||
)
|
||||
|
||||
repo = AdminUiPreferenceRepositoryImpl(db)
|
||||
state = parse_search_state(
|
||||
await repo.get(admin.id, PUSH_SEARCH_CONTEXT),
|
||||
max_history=MAX_PUSH_SEARCH_HISTORY,
|
||||
)
|
||||
if payload.history is not None:
|
||||
state = apply_search_replace(
|
||||
history=payload.history,
|
||||
last=payload.last,
|
||||
max_history=MAX_PUSH_SEARCH_HISTORY,
|
||||
)
|
||||
else:
|
||||
state = apply_search_record(
|
||||
state,
|
||||
payload.query or "",
|
||||
max_history=MAX_PUSH_SEARCH_HISTORY,
|
||||
)
|
||||
await repo.upsert(admin.id, PUSH_SEARCH_CONTEXT, state)
|
||||
return state
|
||||
|
||||
|
||||
@router.get("/logs", response_model=dict)
|
||||
async def get_all_sync_logs(
|
||||
server_id: Optional[int] = Query(None, description="Filter by server ID"),
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
MAX_SERVER_SEARCH_HISTORY = 12
|
||||
MAX_PUSH_SEARCH_HISTORY = 5
|
||||
MAX_SERVER_SEARCH_QUERY_LEN = 200
|
||||
SERVERS_SEARCH_CONTEXT = "servers_search"
|
||||
PUSH_SEARCH_CONTEXT = "push_search"
|
||||
|
||||
|
||||
def normalize_search_query(query: str | None) -> str:
|
||||
@@ -15,7 +17,7 @@ def empty_search_state() -> dict:
|
||||
return {"history": [], "last": None}
|
||||
|
||||
|
||||
def parse_search_state(raw: dict | None) -> dict:
|
||||
def parse_search_state(raw: dict | None, *, max_history: int = MAX_SERVER_SEARCH_HISTORY) -> dict:
|
||||
if not raw:
|
||||
return empty_search_state()
|
||||
history_raw = raw.get("history")
|
||||
@@ -27,7 +29,7 @@ def parse_search_state(raw: dict | None) -> dict:
|
||||
q = normalize_search_query(item)
|
||||
if q and q not in history:
|
||||
history.append(q)
|
||||
if len(history) >= MAX_SERVER_SEARCH_HISTORY:
|
||||
if len(history) >= max_history:
|
||||
break
|
||||
last_raw = raw.get("last")
|
||||
last = normalize_search_query(last_raw) if isinstance(last_raw, str) else ""
|
||||
@@ -35,22 +37,27 @@ def parse_search_state(raw: dict | None) -> dict:
|
||||
return {"history": history, "last": last}
|
||||
|
||||
|
||||
def apply_search_record(state: dict, query: str) -> dict:
|
||||
def apply_search_record(state: dict, query: str, *, max_history: int = MAX_SERVER_SEARCH_HISTORY) -> dict:
|
||||
q = normalize_search_query(query)
|
||||
history = list(state.get("history") or [])
|
||||
if not q:
|
||||
return {"history": history, "last": None}
|
||||
history = [q] + [x for x in history if x != q][:MAX_SERVER_SEARCH_HISTORY - 1]
|
||||
history = [q] + [x for x in history if x != q][: max_history - 1]
|
||||
return {"history": history, "last": q}
|
||||
|
||||
|
||||
def apply_search_replace(*, history: list[str], last: str | None = None) -> dict:
|
||||
def apply_search_replace(
|
||||
*,
|
||||
history: list[str],
|
||||
last: str | None = None,
|
||||
max_history: int = MAX_SERVER_SEARCH_HISTORY,
|
||||
) -> dict:
|
||||
cleaned: list[str] = []
|
||||
for item in history:
|
||||
q = normalize_search_query(item)
|
||||
if q and q not in cleaned:
|
||||
cleaned.append(q)
|
||||
if len(cleaned) >= MAX_SERVER_SEARCH_HISTORY:
|
||||
if len(cleaned) >= max_history:
|
||||
break
|
||||
last_norm = normalize_search_query(last) if last else ""
|
||||
last_val = last_norm or (cleaned[0] if cleaned else None)
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from server.utils.server_search_history import (
|
||||
MAX_PUSH_SEARCH_HISTORY,
|
||||
MAX_SERVER_SEARCH_HISTORY,
|
||||
apply_search_record,
|
||||
parse_search_state,
|
||||
@@ -37,6 +38,36 @@ def test_parse_search_state_normalizes():
|
||||
assert parsed["last"] == "x"
|
||||
|
||||
|
||||
def test_push_search_history_caps_at_five():
|
||||
raw = {"history": [f"q{i}" for i in range(8)], "last": "q0"}
|
||||
parsed = parse_search_state(raw, max_history=MAX_PUSH_SEARCH_HISTORY)
|
||||
assert len(parsed["history"]) == MAX_PUSH_SEARCH_HISTORY
|
||||
|
||||
state = {"history": [f"q{i}" for i in range(MAX_PUSH_SEARCH_HISTORY)], "last": "q0"}
|
||||
capped = apply_search_record(state, "new", max_history=MAX_PUSH_SEARCH_HISTORY)
|
||||
assert capped["history"][0] == "new"
|
||||
assert len(capped["history"]) == MAX_PUSH_SEARCH_HISTORY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_push_search_history_api_roundtrip(integration_env, api_client, auth_headers):
|
||||
r = await api_client.get("/api/servers/push-search-history", headers=auth_headers)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"history": [], "last": None}
|
||||
|
||||
for i in range(6):
|
||||
r = await api_client.put(
|
||||
"/api/servers/push-search-history",
|
||||
headers=auth_headers,
|
||||
json={"query": f"push-{i}"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
body = r.json()
|
||||
assert len(body["history"]) == MAX_PUSH_SEARCH_HISTORY
|
||||
assert body["history"][0] == "push-5"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_history_api_roundtrip(integration_env, api_client, auth_headers):
|
||||
r = await api_client.get("/api/servers/search-history", headers=auth_headers)
|
||||
|
||||
Reference in New Issue
Block a user