Scope skill usage to request profile
This commit is contained in:
@@ -7,8 +7,13 @@ import {
|
|||||||
} from '../../services/config-helpers'
|
} from '../../services/config-helpers'
|
||||||
import { pinSkill } from '../../services/hermes/hermes-cli'
|
import { pinSkill } from '../../services/hermes/hermes-cli'
|
||||||
import { isPathWithin } from '../../services/hermes/hermes-path'
|
import { isPathWithin } from '../../services/hermes/hermes-path'
|
||||||
|
import { getActiveProfileName } from '../../services/hermes/hermes-profile'
|
||||||
import { getSkillUsageStatsFromDb } from '../../db/hermes/sessions-db'
|
import { getSkillUsageStatsFromDb } from '../../db/hermes/sessions-db'
|
||||||
|
|
||||||
|
function requestedProfile(ctx: any): string {
|
||||||
|
return ctx.state?.profile?.name || getActiveProfileName() || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
/** Read bundled manifest as a name→hash map from ~/.hermes/skills/.bundled_manifest */
|
/** Read bundled manifest as a name→hash map from ~/.hermes/skills/.bundled_manifest */
|
||||||
function readBundledManifest(manifestContent: string | null): Map<string, string> {
|
function readBundledManifest(manifestContent: string | null): Map<string, string> {
|
||||||
const map = new Map<string, string>()
|
const map = new Map<string, string>()
|
||||||
@@ -284,7 +289,7 @@ export async function usageStats(ctx: any) {
|
|||||||
const days = Number.isFinite(rawDays) && rawDays > 0 ? Math.min(rawDays, 365) : 7
|
const days = Number.isFinite(rawDays) && rawDays > 0 ? Math.min(rawDays, 365) : 7
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctx.body = await getSkillUsageStatsFromDb(days)
|
ctx.body = await getSkillUsageStatsFromDb(days, undefined, requestedProfile(ctx))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.status = 500
|
ctx.status = 500
|
||||||
ctx.body = { error: `Failed to read skill usage stats: ${err.message}` }
|
ctx.body = { error: `Failed to read skill usage stats: ${err.message}` }
|
||||||
|
|||||||
@@ -953,11 +953,12 @@ function formatUnixDate(timestamp: number | null): string {
|
|||||||
export async function getSkillUsageStatsFromDb(
|
export async function getSkillUsageStatsFromDb(
|
||||||
days = 7,
|
days = 7,
|
||||||
nowSeconds = Math.floor(Date.now() / 1000),
|
nowSeconds = Math.floor(Date.now() / 1000),
|
||||||
|
profile?: string,
|
||||||
): Promise<HermesSkillUsageStats> {
|
): Promise<HermesSkillUsageStats> {
|
||||||
const normalizedDays = Number.isFinite(days) ? days : 7
|
const normalizedDays = Number.isFinite(days) ? days : 7
|
||||||
const safeDays = Math.max(1, Math.floor(normalizedDays))
|
const safeDays = Math.max(1, Math.floor(normalizedDays))
|
||||||
const since = nowSeconds - safeDays * 24 * 60 * 60
|
const since = nowSeconds - safeDays * 24 * 60 * 60
|
||||||
const db = await openSessionDb()
|
const db = await openSessionDb(profile)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hasStartedIndex = db.prepare("PRAGMA index_list(sessions)").all()
|
const hasStartedIndex = db.prepare("PRAGMA index_list(sessions)").all()
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const mockGetSkillUsageStatsFromDb = vi.hoisted(() => vi.fn())
|
||||||
|
const mockGetActiveProfileName = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
vi.mock('../../packages/server/src/db/hermes/sessions-db', () => ({
|
||||||
|
getSkillUsageStatsFromDb: mockGetSkillUsageStatsFromDb,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
|
||||||
|
getActiveProfileName: mockGetActiveProfileName,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({
|
||||||
|
pinSkill: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function loadController() {
|
||||||
|
vi.resetModules()
|
||||||
|
return import('../../packages/server/src/controllers/hermes/skills')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('skills controller', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockGetActiveProfileName.mockReturnValue('default')
|
||||||
|
mockGetSkillUsageStatsFromDb.mockResolvedValue({
|
||||||
|
period_days: 7,
|
||||||
|
summary: {
|
||||||
|
total_skill_loads: 0,
|
||||||
|
total_skill_edits: 0,
|
||||||
|
total_skill_actions: 0,
|
||||||
|
distinct_skills_used: 0,
|
||||||
|
},
|
||||||
|
by_day: [],
|
||||||
|
top_skills: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads skill usage from the request-scoped profile state database', async () => {
|
||||||
|
const { usageStats } = await loadController()
|
||||||
|
const ctx: any = { query: { days: '30' }, state: { profile: { name: 'research' } }, body: null }
|
||||||
|
|
||||||
|
await usageStats(ctx)
|
||||||
|
|
||||||
|
expect(mockGetSkillUsageStatsFromDb).toHaveBeenCalledWith(30, undefined, 'research')
|
||||||
|
expect(ctx.body.period_days).toBe(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to active profile when no request profile is set', async () => {
|
||||||
|
mockGetActiveProfileName.mockReturnValue('travel')
|
||||||
|
const { usageStats } = await loadController()
|
||||||
|
const ctx: any = { query: {}, state: {}, body: null }
|
||||||
|
|
||||||
|
await usageStats(ctx)
|
||||||
|
|
||||||
|
expect(mockGetSkillUsageStatsFromDb).toHaveBeenCalledWith(7, undefined, 'travel')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user