mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-02 09:24:48 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
import { MOCK_TEAM_ID } from 'lib/api.mock'
|
|
|
|
import { combineUrl, router } from 'kea-router'
|
|
import { expectLogic } from 'kea-test-utils'
|
|
|
|
import { addProjectIdIfMissing } from 'lib/utils/kea-router'
|
|
import { sceneLogic } from 'scenes/sceneLogic'
|
|
import { urls } from 'scenes/urls'
|
|
|
|
import { NodeKind, SessionQuery } from '~/queries/schema/schema-general'
|
|
import { initKeaTests } from '~/test/init'
|
|
|
|
import { aiObservabilitySessionLogic } from './aiObservabilitySessionLogic'
|
|
|
|
const blankScene = (): any => ({ scene: { component: () => null, logic: null } })
|
|
const scenes: any = { AIObservabilitySession: blankScene }
|
|
|
|
describe('aiObservabilitySessionLogic', () => {
|
|
let logic: ReturnType<typeof aiObservabilitySessionLogic.build>
|
|
|
|
beforeEach(async () => {
|
|
initKeaTests()
|
|
sceneLogic({ scenes }).mount()
|
|
logic = aiObservabilitySessionLogic()
|
|
logic.mount()
|
|
})
|
|
|
|
afterEach(() => {
|
|
logic.unmount()
|
|
})
|
|
|
|
describe('URL routing', () => {
|
|
it('properly loads session scene with basic session ID', async () => {
|
|
const sessionId = 'session-123'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionId))
|
|
const finalUrl = addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID)
|
|
|
|
router.actions.push(finalUrl)
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
expect(logic.values.sessionId).toBe(sessionId)
|
|
})
|
|
|
|
it('properly loads session scene when session ID contains a colon', async () => {
|
|
const sessionIdWithColon = 'session:group:16-16:81008d53ff0a708b'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionIdWithColon))
|
|
const finalUrl = addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID)
|
|
|
|
router.actions.push(finalUrl)
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
expect(logic.values.sessionId).toBe(sessionIdWithColon)
|
|
})
|
|
|
|
it('properly loads session scene when session ID contains multiple colons', async () => {
|
|
const sessionIdWithMultipleColons = 'namespace:session:12345:abcdef:xyz'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionIdWithMultipleColons))
|
|
|
|
router.actions.push(addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID))
|
|
await expectLogic(logic).toMatchValues({
|
|
sessionId: sessionIdWithMultipleColons,
|
|
})
|
|
})
|
|
|
|
it('handles session ID with timestamp parameter', async () => {
|
|
const sessionId = 'session-456'
|
|
const timestamp = '2024-01-01T00:00:00Z'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionId, { timestamp }))
|
|
|
|
router.actions.push(addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID))
|
|
await expectLogic(logic).toMatchValues({
|
|
sessionId: sessionId,
|
|
dateRange: { dateFrom: timestamp, dateTo: null },
|
|
})
|
|
})
|
|
|
|
it('handles session ID with UUID format', async () => {
|
|
const sessionId = '550e8400-e29b-41d4-a716-446655440000'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionId))
|
|
|
|
router.actions.push(addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID))
|
|
await expectLogic(logic).toMatchValues({
|
|
sessionId: sessionId,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('state management', () => {
|
|
it('has correct initial state', () => {
|
|
expect(logic.values.sessionId).toBe('')
|
|
expect(logic.values.dateRange).toBeNull()
|
|
})
|
|
|
|
it('sets session ID via action', async () => {
|
|
await expectLogic(logic, () => {
|
|
logic.actions.setSessionId('test-session-789')
|
|
}).toMatchValues({
|
|
sessionId: 'test-session-789',
|
|
})
|
|
})
|
|
|
|
it('sets date range via action', async () => {
|
|
await expectLogic(logic, () => {
|
|
logic.actions.setDateRange('2024-01-01T00:00:00Z', '2024-01-31T23:59:59Z')
|
|
}).toMatchValues({
|
|
dateRange: {
|
|
dateFrom: '2024-01-01T00:00:00Z',
|
|
dateTo: '2024-01-31T23:59:59Z',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('handles null date range values', async () => {
|
|
await expectLogic(logic, () => {
|
|
logic.actions.setDateRange(null, null)
|
|
}).toMatchValues({
|
|
dateRange: {
|
|
dateFrom: null,
|
|
dateTo: null,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('handles partial date range (only dateFrom)', async () => {
|
|
await expectLogic(logic, () => {
|
|
logic.actions.setDateRange('2024-01-01T00:00:00Z')
|
|
}).toMatchValues({
|
|
dateRange: {
|
|
dateFrom: '2024-01-01T00:00:00Z',
|
|
dateTo: null,
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('query selector', () => {
|
|
it('generates correct SessionQuery with session ID', async () => {
|
|
logic.actions.setSessionId('test-session-123')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const query = logic.values.query
|
|
const source = query.source as SessionQuery
|
|
|
|
expect(query.kind).toBe(NodeKind.DataTableNode)
|
|
expect(source.kind).toBe(NodeKind.SessionQuery)
|
|
expect(source.sessionId).toBe('test-session-123')
|
|
expect(source.includeSentiment).toBe(true)
|
|
})
|
|
|
|
it('includes date range in query when dateRange is set', async () => {
|
|
logic.actions.setSessionId('test-session-456')
|
|
logic.actions.setDateRange('2024-01-01T00:00:00Z', '2024-01-31T23:59:59Z')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const query = logic.values.query
|
|
const source = query.source as SessionQuery
|
|
|
|
expect(source.dateRange).toEqual({
|
|
date_from: '2024-01-01T00:00:00Z',
|
|
date_to: '2024-01-31T23:59:59Z',
|
|
})
|
|
})
|
|
|
|
it('generates date range when dateRange has dateFrom but no dateTo', async () => {
|
|
logic.actions.setSessionId('test-session-789')
|
|
logic.actions.setDateRange('2024-01-01T00:00:00Z')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const query = logic.values.query
|
|
const source = query.source as SessionQuery
|
|
|
|
expect(source.dateRange?.date_from).toBe('2024-01-01T00:00:00Z')
|
|
// date_to is optional when only date_from is provided
|
|
expect(source.dateRange?.date_to).toBeUndefined()
|
|
})
|
|
|
|
it('generates query without dateRange when no dateRange is explicitly set', async () => {
|
|
logic.actions.setSessionId('test-session-no-dates')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const query = logic.values.query
|
|
const source = query.source as SessionQuery
|
|
|
|
// When no explicit dateRange is set, the query should not have a dateRange
|
|
// (it will use the global dateFilter from parent logic when displayed)
|
|
expect(source.dateRange).toBeUndefined()
|
|
})
|
|
|
|
it('updates query when session ID changes', async () => {
|
|
logic.actions.setSessionId('session-1')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
let query = logic.values.query
|
|
let source = query.source as SessionQuery
|
|
expect(source.sessionId).toBe('session-1')
|
|
|
|
logic.actions.setSessionId('session-2')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
query = logic.values.query
|
|
source = query.source as SessionQuery
|
|
expect(source.sessionId).toBe('session-2')
|
|
})
|
|
})
|
|
|
|
describe('breadcrumbs selector', () => {
|
|
it('generates correct breadcrumbs structure', async () => {
|
|
logic.actions.setSessionId('test-session-breadcrumbs')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const breadcrumbs = logic.values.breadcrumbs
|
|
|
|
expect(breadcrumbs).toHaveLength(3)
|
|
expect(breadcrumbs[0]).toEqual({
|
|
key: 'AIObservability',
|
|
name: 'AI observability',
|
|
path: urls.aiObservabilityDashboard(),
|
|
iconType: 'llm_analytics',
|
|
})
|
|
// Breadcrumbs path includes the date filter from parent logic
|
|
expect(breadcrumbs[1].key).toBe('AIObservabilitySessions')
|
|
expect(breadcrumbs[1].name).toBe('Sessions')
|
|
expect(breadcrumbs[1].path).toContain(urls.aiObservabilitySessions())
|
|
expect(breadcrumbs[1].iconType).toBe('llm_analytics')
|
|
expect(breadcrumbs[2]).toEqual({
|
|
key: ['AIObservabilitySession', 'test-session-breadcrumbs'],
|
|
name: 'test-session-breadcrumbs',
|
|
iconType: 'llm_analytics',
|
|
})
|
|
})
|
|
|
|
it('updates breadcrumbs when session ID changes', async () => {
|
|
logic.actions.setSessionId('session-1')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
let breadcrumbs = logic.values.breadcrumbs
|
|
expect(breadcrumbs[2].name).toBe('session-1')
|
|
|
|
logic.actions.setSessionId('session-2')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
breadcrumbs = logic.values.breadcrumbs
|
|
expect(breadcrumbs[2].name).toBe('session-2')
|
|
})
|
|
|
|
it('handles empty session ID in breadcrumbs', async () => {
|
|
logic.actions.setSessionId('')
|
|
await expectLogic(logic).toFinishAllListeners()
|
|
|
|
const breadcrumbs = logic.values.breadcrumbs
|
|
expect(breadcrumbs[2]).toEqual({
|
|
key: ['AIObservabilitySession', ''],
|
|
name: '',
|
|
iconType: 'llm_analytics',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('integration with URL', () => {
|
|
it('extracts both session ID and timestamp from URL', async () => {
|
|
const sessionId = 'full-integration-test'
|
|
const timestamp = '2024-06-15T12:00:00Z'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionId, { timestamp }))
|
|
|
|
router.actions.push(addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID))
|
|
await expectLogic(logic).toMatchValues({
|
|
sessionId: sessionId,
|
|
dateRange: {
|
|
dateFrom: timestamp,
|
|
dateTo: null,
|
|
},
|
|
})
|
|
|
|
// Query should reflect both values
|
|
const query = logic.values.query
|
|
const source = query.source as SessionQuery
|
|
expect(source.sessionId).toBe(sessionId)
|
|
expect(source.dateRange?.date_from).toBe(timestamp)
|
|
})
|
|
|
|
it('handles URL navigation without timestamp', async () => {
|
|
const sessionId = 'no-timestamp-test'
|
|
const sessionUrl = combineUrl(urls.aiObservabilitySession(sessionId))
|
|
|
|
router.actions.push(addProjectIdIfMissing(sessionUrl.url, MOCK_TEAM_ID))
|
|
await expectLogic(logic).toMatchValues({
|
|
sessionId: sessionId,
|
|
// When navigating without explicit date params, falls back to parent dateFilter
|
|
dateRange: { dateFrom: '-1h', dateTo: null },
|
|
})
|
|
})
|
|
})
|
|
})
|