mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +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.
114 lines
5.0 KiB
TypeScript
114 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import setActiveProjectTool from '@/tools/projects/setActive'
|
|
import type { Context } from '@/tools/types'
|
|
|
|
const ACTIVE_ORG = 'org-active'
|
|
const OTHER_ORG = 'org-other'
|
|
|
|
function createMockContext(overrides: {
|
|
projectGet: ReturnType<typeof vi.fn>
|
|
getOrgID?: ReturnType<typeof vi.fn>
|
|
getCachedOrFetchOrg?: ReturnType<typeof vi.fn>
|
|
}): { context: Context; cache: Map<string, unknown>; getCachedOrFetchOrg: ReturnType<typeof vi.fn> } {
|
|
const cache = new Map<string, unknown>()
|
|
const getCachedOrFetchOrg = overrides.getCachedOrFetchOrg ?? vi.fn().mockResolvedValue(undefined)
|
|
const context = {
|
|
api: {
|
|
publicBaseUrl: 'https://us.hanzo.ai',
|
|
projects: () => ({ get: overrides.projectGet }),
|
|
},
|
|
cache: {
|
|
get: async (key: string) => cache.get(key),
|
|
set: async (key: string, value: unknown) => {
|
|
cache.set(key, value)
|
|
},
|
|
},
|
|
stateManager: {
|
|
getOrgID: overrides.getOrgID ?? vi.fn().mockResolvedValue(ACTIVE_ORG),
|
|
getCachedOrFetchOrg,
|
|
},
|
|
env: {},
|
|
sessionManager: {},
|
|
getDistinctId: async () => 'test-distinct-id',
|
|
trackEvent: async () => {},
|
|
} as unknown as Context
|
|
return { context, cache, getCachedOrFetchOrg }
|
|
}
|
|
|
|
describe('switch-project', () => {
|
|
const tool = setActiveProjectTool()
|
|
|
|
it('does not commit the session, preserves the error cause, and guides the agent when the project is unreachable', async () => {
|
|
const apiError = new Error('404')
|
|
const projectGet = vi.fn().mockResolvedValue({ success: false, error: apiError })
|
|
const { context, cache } = createMockContext({ projectGet })
|
|
|
|
let caught: (Error & { cause?: unknown }) | undefined
|
|
try {
|
|
await tool.handler(context, { projectId: 999 })
|
|
} catch (e) {
|
|
caught = e as Error & { cause?: unknown }
|
|
}
|
|
|
|
expect(caught?.message).toMatch(
|
|
/Could not switch to project 999.*active organization \(org-active\).*organizations-get.*switch-organization/s
|
|
)
|
|
// Keeping the typed error as cause keeps recoverable not-found/no-access
|
|
// failures out of exception tracking.
|
|
expect(caught?.cause).toBe(apiError)
|
|
// A failed switch must not strand the session on an unreachable project.
|
|
expect(cache.get('projectId')).toBeUndefined()
|
|
})
|
|
|
|
it('switches within the active organization without touching org state', async () => {
|
|
const projectGet = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
data: { id: 42, name: 'My project', organization: ACTIVE_ORG },
|
|
})
|
|
const { context, cache, getCachedOrFetchOrg } = createMockContext({ projectGet })
|
|
|
|
const result = await tool.handler(context, { projectId: 42 })
|
|
|
|
expect(result.content[0]!.text).toContain('Switched to project 42')
|
|
expect(result.content[0]!.text).not.toContain('also switched the active organization')
|
|
expect(cache.get('projectId')).toBe('42')
|
|
expect(getCachedOrFetchOrg).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('syncs the active organization via the shared resolver when the project is in another org', async () => {
|
|
const projectGet = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
data: { id: 77, name: 'Cross-org project', organization: OTHER_ORG },
|
|
})
|
|
const getCachedOrFetchOrg = vi.fn().mockResolvedValue({ id: OTHER_ORG, name: 'Other org' })
|
|
const { context, cache } = createMockContext({ projectGet, getCachedOrFetchOrg })
|
|
|
|
const result = await tool.handler(context, { projectId: 77 })
|
|
|
|
expect(result.content[0]!.text).toContain('also switched the active organization to org-other')
|
|
expect(cache.get('projectId')).toBe('77')
|
|
expect(cache.get('orgId')).toBe(OTHER_ORG)
|
|
// Reuses the resolver (with its scoped-token guard) instead of calling the
|
|
// org endpoint directly, which the backend rejects for project-scoped keys.
|
|
expect(getCachedOrFetchOrg).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('establishes org context without claiming a switch when no active org was resolved', async () => {
|
|
const projectGet = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
data: { id: 55, name: 'Scoped project', organization: OTHER_ORG },
|
|
})
|
|
// Team-scoped keys resolve no org on the first call, and getCachedOrFetchOrg
|
|
// returns undefined for them (the org endpoint is rejected for such tokens).
|
|
const getOrgID = vi.fn().mockRejectedValue(new Error('no org'))
|
|
const { context, cache } = createMockContext({ projectGet, getOrgID })
|
|
|
|
const result = await tool.handler(context, { projectId: 55 })
|
|
|
|
expect(result.content[0]!.text).toContain('Switched to project 55')
|
|
expect(result.content[0]!.text).not.toContain('also switched the active organization')
|
|
expect(cache.get('orgId')).toBe(OTHER_ORG)
|
|
})
|
|
})
|