From 4db3940e651a1b5ea81fdb18a59f5df06cdf26e9 Mon Sep 17 00:00:00 2001 From: ekko Date: Sun, 24 May 2026 08:48:40 +0800 Subject: [PATCH] Scope skill usage to request profile --- .../server/src/controllers/hermes/skills.ts | 7 ++- packages/server/src/db/hermes/sessions-db.ts | 3 +- tests/server/skills-controller.test.ts | 59 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/server/skills-controller.test.ts diff --git a/packages/server/src/controllers/hermes/skills.ts b/packages/server/src/controllers/hermes/skills.ts index 5122fd6..45e1ee8 100644 --- a/packages/server/src/controllers/hermes/skills.ts +++ b/packages/server/src/controllers/hermes/skills.ts @@ -7,8 +7,13 @@ import { } from '../../services/config-helpers' import { pinSkill } from '../../services/hermes/hermes-cli' import { isPathWithin } from '../../services/hermes/hermes-path' +import { getActiveProfileName } from '../../services/hermes/hermes-profile' 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 */ function readBundledManifest(manifestContent: string | null): Map { const map = new Map() @@ -284,7 +289,7 @@ export async function usageStats(ctx: any) { const days = Number.isFinite(rawDays) && rawDays > 0 ? Math.min(rawDays, 365) : 7 try { - ctx.body = await getSkillUsageStatsFromDb(days) + ctx.body = await getSkillUsageStatsFromDb(days, undefined, requestedProfile(ctx)) } catch (err: any) { ctx.status = 500 ctx.body = { error: `Failed to read skill usage stats: ${err.message}` } diff --git a/packages/server/src/db/hermes/sessions-db.ts b/packages/server/src/db/hermes/sessions-db.ts index 6f0f64c..66dca39 100644 --- a/packages/server/src/db/hermes/sessions-db.ts +++ b/packages/server/src/db/hermes/sessions-db.ts @@ -953,11 +953,12 @@ function formatUnixDate(timestamp: number | null): string { export async function getSkillUsageStatsFromDb( days = 7, nowSeconds = Math.floor(Date.now() / 1000), + profile?: string, ): Promise { const normalizedDays = Number.isFinite(days) ? days : 7 const safeDays = Math.max(1, Math.floor(normalizedDays)) const since = nowSeconds - safeDays * 24 * 60 * 60 - const db = await openSessionDb() + const db = await openSessionDb(profile) try { const hasStartedIndex = db.prepare("PRAGMA index_list(sessions)").all() diff --git a/tests/server/skills-controller.test.ts b/tests/server/skills-controller.test.ts new file mode 100644 index 0000000..f281cd6 --- /dev/null +++ b/tests/server/skills-controller.test.ts @@ -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') + }) +})