feat(agent): move 'Vad din agent vet' into settings (Assistenten -> Kunskap) (#1008)
Relocates the ledger-knowledge surface off the top nav and into the assistant settings hub as a third tab (Minne / Kompetens / Kunskap), per the code's own "minne + kunskap under Assistenten" intent and the #935 flag that this was an easy call to change. Because both settings surfaces (the full-page rail and the intercepting settings modal) mount each section as a propless component via SETTINGS_SECTIONS, the knowledge data must be fetched client-side rather than passed as a server prop: - New GET /api/agent/knowledge aggregates buildLedgerContext + buildDeepEntities + buildAgentCompetence + company name (read-only, company-scoped via withRouteContext). - AgentKnowledgeView + AgentCompetenceSections converted from async server components to client components (getTranslations -> useTranslations; no other server-only usage). - New AgentKnowledgePanel client wrapper lazy-fetches the payload when the Kunskap tab opens (Radix unmounts inactive tabs), with Skeleton and error states matching the memory/skills panels. - Removed the Brain/agent-knowledge entry (and its now-unused import) from the Analys nav group. - /agent-knowledge kept as a redirect to /settings/assistant?view=knowledge so old links/bookmarks resolve. Tests: new route test (auth 401, no-company 400, happy-path aggregation). i18n: load_error_* keys added to both locales (parity kept). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4b51af3d80
commit
dcd33997b7
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Tests for GET /api/agent/knowledge (the "Vad din agent vet" data source for
|
||||
* the Kunskap tab in the assistant settings hub). Exercises the route through
|
||||
* the real withRouteContext wrapper, mocking auth/company and the three
|
||||
* ledger-context builders. Covers auth 401, no-company 400, and the happy path
|
||||
* aggregation shape.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createQueuedMockSupabase, createMockRequest, parseJsonResponse } from '@/tests/helpers'
|
||||
|
||||
const { supabase } = createQueuedMockSupabase()
|
||||
|
||||
const requireAuthMock = vi.fn()
|
||||
vi.mock('@/lib/auth/require-auth', () => ({
|
||||
requireAuth: (...args: unknown[]) => requireAuthMock(...args),
|
||||
}))
|
||||
|
||||
const getActiveCompanyIdMock = vi.fn()
|
||||
const getCompanyDisplayNameMock = vi.fn()
|
||||
vi.mock('@/lib/company/context', () => ({
|
||||
getActiveCompanyId: (...args: unknown[]) => getActiveCompanyIdMock(...args),
|
||||
getCompanyDisplayName: (...args: unknown[]) => getCompanyDisplayNameMock(...args),
|
||||
}))
|
||||
|
||||
const buildLedgerContextMock = vi.fn()
|
||||
vi.mock('@/lib/agent-context/ledger-context', () => ({
|
||||
buildLedgerContext: (...args: unknown[]) => buildLedgerContextMock(...args),
|
||||
}))
|
||||
|
||||
const buildDeepEntitiesMock = vi.fn()
|
||||
vi.mock('@/lib/agent-context/ledger-deep', () => ({
|
||||
buildDeepEntities: (...args: unknown[]) => buildDeepEntitiesMock(...args),
|
||||
}))
|
||||
|
||||
const buildAgentCompetenceMock = vi.fn()
|
||||
vi.mock('@/lib/agent-context/agent-competence', () => ({
|
||||
buildAgentCompetence: (...args: unknown[]) => buildAgentCompetenceMock(...args),
|
||||
}))
|
||||
|
||||
import { GET } from '../route'
|
||||
|
||||
describe('GET /api/agent/knowledge', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase, error: null })
|
||||
getActiveCompanyIdMock.mockResolvedValue('company-1')
|
||||
getCompanyDisplayNameMock.mockResolvedValue('Acme AB')
|
||||
buildLedgerContextMock.mockResolvedValue({ meta: { coverage: {} }, explicit_rules: [], vat_profile: {}, conventions: {} })
|
||||
buildDeepEntitiesMock.mockResolvedValue({ counterparty_entities: [], supplier_entities: [] })
|
||||
buildAgentCompetenceMock.mockResolvedValue({ atoms: [], facts: [] })
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
requireAuthMock.mockResolvedValue({
|
||||
user: null,
|
||||
supabase,
|
||||
error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
|
||||
})
|
||||
const res = await GET(createMockRequest('/api/agent/knowledge'), { params: Promise.resolve({}) })
|
||||
const { status } = await parseJsonResponse(res)
|
||||
expect(status).toBe(401)
|
||||
})
|
||||
|
||||
it('returns 400 when there is no active company', async () => {
|
||||
getActiveCompanyIdMock.mockResolvedValue(null)
|
||||
const res = await GET(createMockRequest('/api/agent/knowledge'), { params: Promise.resolve({}) })
|
||||
const { status } = await parseJsonResponse(res)
|
||||
expect(status).toBe(400)
|
||||
})
|
||||
|
||||
it('aggregates the ledger context, deep entities, competence and company name', async () => {
|
||||
const res = await GET(createMockRequest('/api/agent/knowledge'), { params: Promise.resolve({}) })
|
||||
const { status, body } = await parseJsonResponse<{
|
||||
data: { context: unknown; deep: unknown; competence: unknown; companyName: string }
|
||||
}>(res)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data.companyName).toBe('Acme AB')
|
||||
expect(body.data.context).toEqual({ meta: { coverage: {} }, explicit_rules: [], vat_profile: {}, conventions: {} })
|
||||
expect(body.data.deep).toEqual({ counterparty_entities: [], supplier_entities: [] })
|
||||
expect(body.data.competence).toEqual({ atoms: [], facts: [] })
|
||||
// each builder was called with the resolved supabase + companyId
|
||||
expect(buildLedgerContextMock).toHaveBeenCalledWith(supabase, 'company-1')
|
||||
expect(buildDeepEntitiesMock).toHaveBeenCalledWith(supabase, 'company-1')
|
||||
expect(buildAgentCompetenceMock).toHaveBeenCalledWith(supabase, 'company-1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { withRouteContext } from '@/lib/api/with-route-context'
|
||||
import { getCompanyDisplayName } from '@/lib/company/context'
|
||||
import { buildLedgerContext } from '@/lib/agent-context/ledger-context'
|
||||
import { buildDeepEntities } from '@/lib/agent-context/ledger-deep'
|
||||
import { buildAgentCompetence } from '@/lib/agent-context/agent-competence'
|
||||
|
||||
// GET /api/agent/knowledge
|
||||
//
|
||||
// Read-only transparency surface for "Vad din agent vet": the exact ledger
|
||||
// context the AI agent reads before booking, plus the deep entity-resolved
|
||||
// profile and the agent's competence. Powers the Kunskap tab in the assistant
|
||||
// settings hub (client-fetched so it renders identically in the full-page
|
||||
// settings rail and the routed settings modal, which both mount the same
|
||||
// propless content component). Derived per request from live bookings; never
|
||||
// cached, so a fixed profile can't go stale.
|
||||
export const GET = withRouteContext('agent.knowledge.get', async (_request, ctx) => {
|
||||
const { supabase, companyId } = ctx
|
||||
|
||||
const [context, deep, competence, companyName] = await Promise.all([
|
||||
buildLedgerContext(supabase, companyId),
|
||||
buildDeepEntities(supabase, companyId),
|
||||
buildAgentCompetence(supabase, companyId),
|
||||
getCompanyDisplayName(supabase, companyId),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
data: { context, deep, competence, companyName: companyName ?? '' },
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user