mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 02:16:08 +00:00
feat: conv visibility
This commit is contained in:
1 parent
6d4d96d12d
commit
82bacfdb2b
18 files changed
+361
-110
No files matched your search
@@ -248,7 +248,7 @@ test.describe('tier-a · chat turn save', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('save_conversation=false skips all four tables', async ({ browser }) => {
|
||||
test('save_conversation=false persists a hidden conversation (kept out of the sidebar)', async ({ browser }) => {
|
||||
const { sub, token } = await newUserContext(browser);
|
||||
|
||||
// Sanity: the user has nothing yet.
|
||||
@@ -266,23 +266,30 @@ test.describe('tier-a · chat turn save', () => {
|
||||
isNoneDoc: true,
|
||||
});
|
||||
expect(result.status).toBe(200);
|
||||
// When save_conversation=false the route emits `{"type":"id","id":"None"}`
|
||||
// (stringified None) — no real UUID persisted.
|
||||
expect(result.conversationId).toBe('None');
|
||||
// save_conversation now controls sidebar visibility, not persistence:
|
||||
// the turn persists, so a real conversation UUID is emitted.
|
||||
expect(result.conversationId).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
|
||||
// Key invariant: conversations + conversation_messages must be empty
|
||||
// for this user. token_usage still fires (it's per-LLM-call, not per
|
||||
// conversation); user_logs also fires (audit log). We only assert on
|
||||
// the two "persistent chat state" tables.
|
||||
// The conversation + its messages persist...
|
||||
expect(
|
||||
await countRows('conversations', {
|
||||
sql: 'user_id = $1',
|
||||
params: [sub],
|
||||
}),
|
||||
).toBe(0);
|
||||
|
||||
).toBe(1);
|
||||
const msgs = await fetchMessagesForUser(sub);
|
||||
expect(msgs).toHaveLength(0);
|
||||
expect(msgs.length).toBeGreaterThan(0);
|
||||
|
||||
// ...but it is hidden, so it never surfaces in the sidebar
|
||||
// (list_for_user filters visibility = 'listed').
|
||||
expect(
|
||||
await countRows('conversations', {
|
||||
sql: "user_id = $1 AND visibility = 'listed'",
|
||||
params: [sub],
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('silent-break: two concurrent /stream calls on the same conversation_id — neither 500s and positions stay unique', async ({
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
/**
|
||||
* Tier-B · agent preview does NOT persist conversations
|
||||
* Tier-B · agent preview persists a HIDDEN conversation
|
||||
*
|
||||
* Covers B14. The preview flow in `frontend/src/agents/agentPreviewSlice.ts`
|
||||
* calls `handleFetchAnswerSteaming(..., save_conversation=false)` — see the
|
||||
* `false` positional arg at line 126. That flag flows through
|
||||
* `POST /stream` (application/api/answer/routes/stream.py:55-59,142) into
|
||||
* `BaseAnswerResource.complete_stream(should_save_conversation=...)`
|
||||
* (application/api/answer/routes/base.py:174,290,389,479), where the
|
||||
* branches that insert `conversations` / `conversation_messages` rows are
|
||||
* gated on `should_save_conversation`.
|
||||
* calls `handleFetchAnswerSteaming(..., save_conversation=false)`. As of the
|
||||
* conversation-visibility change, `save_conversation` no longer gates
|
||||
* persistence — it controls sidebar visibility only. A preview turn therefore
|
||||
* persists a `conversations` row with `visibility = 'hidden'` (decided by
|
||||
* `resolve_persistence` in
|
||||
* application/api/answer/services/persistence_policy.py and threaded through
|
||||
* `BaseAnswerResource.complete_stream(visibility=...)`).
|
||||
*
|
||||
* The load-bearing assertion is: after a preview stream completes, the
|
||||
* `conversations` table still has zero rows for this user. If a
|
||||
* regression drops the `save_conversation` flag (or flips the default),
|
||||
* preview traffic would silently pollute the user's sidebar history.
|
||||
* The load-bearing assertion is: after a preview stream completes, the row
|
||||
* exists but is `hidden`, so it is excluded from the sidebar query
|
||||
* (`ConversationsRepository.list_for_user`, which filters `visibility =
|
||||
* 'listed'`). If a regression flips the preview default to `listed`, preview
|
||||
* traffic would pollute the user's sidebar history.
|
||||
*
|
||||
* API-driven: the frontend preview UI ultimately POSTs /stream with
|
||||
* `save_conversation:false` and `conversation_id:null`; we exercise that
|
||||
* contract directly to keep the spec fast and isolated from the
|
||||
* AgentPreview modal's rendering quirks. A standard chat POST
|
||||
* (save_conversation defaulted to true) is included as a control — it
|
||||
* MUST write a conversation row, so "preview wrote zero" is a meaningful
|
||||
* contrast rather than "nothing ever writes".
|
||||
* contract directly to keep the spec fast and isolated from the AgentPreview
|
||||
* modal's rendering quirks. A standard chat POST (save_conversation omitted)
|
||||
* is the control — it writes a `listed` row that DOES surface in the sidebar.
|
||||
*/
|
||||
|
||||
import * as playwright from '@playwright/test';
|
||||
@@ -58,6 +57,16 @@ async function countUserConversations(userSub: string): Promise<number> {
|
||||
return Number(rows[0]?.n ?? 0);
|
||||
}
|
||||
|
||||
/** Conversations that surface in the sidebar (the `list_for_user` filter). */
|
||||
async function countListedConversations(userSub: string): Promise<number> {
|
||||
const { rows } = await pg.query<{ n: string }>(
|
||||
`SELECT count(*)::text AS n FROM conversations
|
||||
WHERE user_id = $1 AND visibility = 'listed'`,
|
||||
[userSub],
|
||||
);
|
||||
return Number(rows[0]?.n ?? 0);
|
||||
}
|
||||
|
||||
async function countConversationMessages(userSub: string): Promise<number> {
|
||||
const { rows } = await pg.query<{ n: string }>(
|
||||
`SELECT count(*)::text AS n
|
||||
@@ -71,10 +80,9 @@ async function countConversationMessages(userSub: string): Promise<number> {
|
||||
|
||||
/**
|
||||
* POST /stream with save_conversation=false and drain the SSE body. The
|
||||
* stream emits `{type:"id", id:"None"}` in preview mode (preview has no
|
||||
* conversation_id to echo back) — we can't use `streamOnce` here because
|
||||
* that helper insists on a UUID-shaped id. Instead, just drain the
|
||||
* response and assert 200.
|
||||
* stream emits `{type:"id", id:"<uuid>"}` once the (hidden) conversation is
|
||||
* persisted. Sidebar membership is asserted via the DB helpers below rather
|
||||
* than the stream id, so we just drain the response and assert 200.
|
||||
*/
|
||||
async function drainStream(
|
||||
api: import('@playwright/test').APIRequestContext,
|
||||
@@ -96,7 +104,7 @@ test.describe('tier-b · agent preview', () => {
|
||||
await resetDb();
|
||||
});
|
||||
|
||||
test('preview stream with save_conversation=false writes NO conversations row', async ({
|
||||
test('preview stream with save_conversation=false persists a hidden conversation excluded from the sidebar', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { context, sub, token } = await newUserContext(browser);
|
||||
@@ -118,14 +126,19 @@ test.describe('tier-b · agent preview', () => {
|
||||
chunks: '0',
|
||||
});
|
||||
|
||||
// Core assertion: preview MUST NOT persist to conversations.
|
||||
// The turn persists (a row + messages exist)...
|
||||
expect(
|
||||
await countUserConversations(sub),
|
||||
'preview stream must not write a conversations row',
|
||||
).toBe(0);
|
||||
'preview stream persists a conversation row',
|
||||
).toBe(1);
|
||||
expect(
|
||||
await countConversationMessages(sub),
|
||||
'preview stream must not write conversation_messages rows',
|
||||
'preview stream persists conversation_messages rows',
|
||||
).toBeGreaterThan(0);
|
||||
// ...but it is hidden, so the sidebar query returns nothing.
|
||||
expect(
|
||||
await countListedConversations(sub),
|
||||
'preview conversation must not surface in the sidebar',
|
||||
).toBe(0);
|
||||
} finally {
|
||||
await api.dispose();
|
||||
@@ -133,7 +146,7 @@ test.describe('tier-b · agent preview', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('control: a non-preview stream (save_conversation omitted) DOES write a conversations row', async ({
|
||||
test('control: a non-preview stream (save_conversation omitted) writes a LISTED conversations row', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { context, sub, token } = await newUserContext(browser);
|
||||
@@ -142,10 +155,10 @@ test.describe('tier-b · agent preview', () => {
|
||||
const agentId = await createDraftAgent(sub, 'non-preview-target');
|
||||
expect(await countUserConversations(sub)).toBe(0);
|
||||
|
||||
// Standard chat path — save_conversation defaults to true at the
|
||||
// backend (stream_model field default + get() fallback in stream.py).
|
||||
// This is the "control" that makes the preview assertion meaningful:
|
||||
// without it, a spec where nothing ever writes could pass trivially.
|
||||
// Standard chat path — save_conversation omitted, so a first-party
|
||||
// interactive turn defaults to `listed` (resolve_persistence). This is
|
||||
// the "control" that makes the preview assertion meaningful: without
|
||||
// it, a spec where nothing ever surfaces could pass trivially.
|
||||
const conversationId = await streamOnce(api, {
|
||||
question: 'Hello persistence',
|
||||
conversation_id: null,
|
||||
@@ -157,8 +170,9 @@ test.describe('tier-b · agent preview', () => {
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
|
||||
// Exactly one conversation row landed.
|
||||
// Exactly one conversation row landed, and it surfaces in the sidebar.
|
||||
expect(await countUserConversations(sub)).toBe(1);
|
||||
expect(await countListedConversations(sub)).toBe(1);
|
||||
expect(await countConversationMessages(sub)).toBeGreaterThan(0);
|
||||
} finally {
|
||||
await api.dispose();
|
||||
@@ -166,7 +180,7 @@ test.describe('tier-b · agent preview', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('multiple preview turns still produce zero conversations rows', async ({
|
||||
test('multiple preview turns persist hidden rows that never surface in the sidebar', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { context, sub, token } = await newUserContext(browser);
|
||||
@@ -186,10 +200,11 @@ test.describe('tier-b · agent preview', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Still zero — no preview turn ever persists, regardless of how
|
||||
// many times the user clicks "Test message".
|
||||
expect(await countUserConversations(sub)).toBe(0);
|
||||
expect(await countConversationMessages(sub)).toBe(0);
|
||||
// Each turn persisted its own hidden conversation...
|
||||
expect(await countUserConversations(sub)).toBe(3);
|
||||
// ...yet the sidebar stays empty no matter how many "Test message"
|
||||
// clicks the user makes.
|
||||
expect(await countListedConversations(sub)).toBe(0);
|
||||
} finally {
|
||||
await api.dispose();
|
||||
await context.close();
|
||||
|
||||
Reference in new issue
Block a user