diff --git a/packages/core/src/__tests__/capability-audit.test.ts b/packages/core/src/__tests__/capability-audit.test.ts new file mode 100644 index 0000000000..1edcece4b2 --- /dev/null +++ b/packages/core/src/__tests__/capability-audit.test.ts @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { deriveCapabilityAuditReport } from '../capability-audit.js'; +import type { ScheduledTask, ScheduledTaskStatus } from '../scheduled-task.js'; + +describe('capability audit facts', () => { + it('reports skill tools and scheduled-task statuses without permission-mode aliases', () => { + const report = deriveCapabilityAuditReport({ + now: 1_000, + skills: [ + { id: 'with-tools', name: 'With tools', declaredTools: ['read', 'write'] }, + { id: 'without-tools', name: 'Without tools' }, + ], + scheduledTasks: [ + scheduledTask('active'), + scheduledTask('paused'), + scheduledTask('completed'), + scheduledTask('expired'), + ], + }); + + assert.deepEqual( + report.skills.map(({ id, hasDeclaredTools }) => ({ id, hasDeclaredTools })), + [ + { id: 'with-tools', hasDeclaredTools: true }, + { id: 'without-tools', hasDeclaredTools: false }, + ], + ); + assert.equal( + report.skills.some((skill) => 'permissionMode' in skill), + false, + ); + assert.deepEqual( + report.scheduledTasks.map(({ id, status }) => ({ id, status })), + [ + { id: 'active', status: 'active' }, + { id: 'paused', status: 'paused' }, + { id: 'completed', status: 'completed' }, + { id: 'expired', status: 'expired' }, + ], + ); + assert.equal( + report.scheduledTasks.some((task) => 'permissionMode' in task), + false, + ); + assert.equal(report.summary.activeScheduledTaskCount, 1); + assert.equal('executableScheduledTaskCount' in report.summary, false); + }); +}); + +function scheduledTask(status: ScheduledTaskStatus): ScheduledTask { + return { + id: status, + title: status, + intent: { kind: 'text', body: '' }, + schedule: { kind: 'once', runAt: 2_000 }, + effect: { kind: 'notify', channel: 'local' }, + status, + nextFireAt: status === 'active' ? 2_000 : null, + lastFireAt: null, + fireCount: 0, + maxFires: null, + expiresAt: null, + createdBy: { kind: 'user' }, + createdAt: 0, + updatedAt: 0, + runs: [], + lastError: null, + }; +} diff --git a/packages/core/src/capability-audit.ts b/packages/core/src/capability-audit.ts index b8a0df8149..5bf366c35c 100644 --- a/packages/core/src/capability-audit.ts +++ b/packages/core/src/capability-audit.ts @@ -17,7 +17,11 @@ * under the License. */ -import type { ScheduledTask, ScheduledTaskRunOutcome } from './scheduled-task.js'; +import type { + ScheduledTask, + ScheduledTaskRunOutcome, + ScheduledTaskStatus, +} from './scheduled-task.js'; export const SOURCE_RECORD_TYPES = ['mcp', 'api', 'local'] as const; export type SourceRecordType = (typeof SOURCE_RECORD_TYPES)[number]; @@ -28,9 +32,6 @@ export type SourceAuthType = (typeof SOURCE_AUTH_TYPES)[number]; export const SOURCE_RECORD_STATUSES = ['ready', 'needs_auth', 'error', 'disabled'] as const; export type SourceRecordStatus = (typeof SOURCE_RECORD_STATUSES)[number]; -export const CAPABILITY_AUDIT_PERMISSION_MODES = ['explore', 'ask'] as const; -export type CapabilityAuditPermissionMode = (typeof CAPABILITY_AUDIT_PERMISSION_MODES)[number]; - export const SCHEDULED_TASK_LAST_RUN_STATUSES = ['ok', 'error', 'skipped'] as const; export type ScheduledTaskLastRunStatus = (typeof SCHEDULED_TASK_LAST_RUN_STATUSES)[number]; @@ -62,16 +63,16 @@ export interface SkillAuditRecord { name: string; description: string; declaredTools: string[]; + hasDeclaredTools: boolean; enabled: boolean; sourceSlug: string; - permissionMode: CapabilityAuditPermissionMode; } export interface ScheduledTaskAuditRecord { id: string; name: string; enabled: boolean; - permissionMode: CapabilityAuditPermissionMode; + status: ScheduledTaskStatus; lastRunAt?: number; lastRunStatus?: ScheduledTaskLastRunStatus; } @@ -88,7 +89,7 @@ export interface CapabilityAuditSummary { declaredToolKindCount: number; scheduledTaskCount: number; enabledScheduledTaskCount: number; - executableScheduledTaskCount: number; + activeScheduledTaskCount: number; failedScheduledTaskCount: number; skippedScheduledTaskCount: number; } @@ -141,9 +142,9 @@ function normalizeSkillInputs(skills: readonly CapabilityAuditSkillInput[]): Ski name: normalizeNonEmptyString(skill.name) ?? id, description: normalizeNonEmptyString(skill.description) ?? '', declaredTools, + hasDeclaredTools: declaredTools.length > 0, enabled: skill.enabled ?? true, sourceSlug: normalizeNonEmptyString(skill.sourceSlug) ?? LOCAL_SKILL_SOURCE_SLUG, - permissionMode: declaredTools.length > 0 ? 'ask' : 'explore', }; }); } @@ -193,19 +194,12 @@ function scheduledTaskToAuditRecord(task: ScheduledTask): ScheduledTaskAuditReco id: task.id, name: task.title, enabled: task.status === 'active', - permissionMode: scheduledTaskPermissionMode(task), + status: task.status, ...(lastRun ? { lastRunAt: lastRun.at } : {}), ...(lastRun ? { lastRunStatus: mapScheduledTaskRunOutcome(lastRun.outcome) } : {}), }; } -function scheduledTaskPermissionMode(task: ScheduledTask): CapabilityAuditPermissionMode { - if (task.status === 'completed' || task.status === 'expired') return 'explore'; - // Active tasks run with `ask` approval — the retired `execute` mode folded to - // `ask` identically, so this preserves the historical behaviour. - return 'ask'; -} - function mapScheduledTaskRunOutcome(outcome: ScheduledTaskRunOutcome): ScheduledTaskLastRunStatus { if (outcome === 'ok') return 'ok'; if (outcome === 'blocked') return 'skipped'; @@ -229,9 +223,7 @@ function summarizeCapabilityAudit( declaredToolKindCount: distinctDeclaredToolKinds(skills).length, scheduledTaskCount: scheduledTasks.length, enabledScheduledTaskCount: scheduledTasks.filter((task) => task.enabled).length, - executableScheduledTaskCount: scheduledTasks.filter( - (task) => task.enabled && task.permissionMode !== 'explore', - ).length, + activeScheduledTaskCount: scheduledTasks.filter((task) => task.status === 'active').length, failedScheduledTaskCount: scheduledTasks.filter((task) => task.lastRunStatus === 'error') .length, skippedScheduledTaskCount: scheduledTasks.filter((task) => task.lastRunStatus === 'skipped') diff --git a/packages/ui/stories/capability-audit-strip.stories.tsx b/packages/ui/stories/capability-audit-strip.stories.tsx index d892c942ef..863c4bdc19 100644 --- a/packages/ui/stories/capability-audit-strip.stories.tsx +++ b/packages/ui/stories/capability-audit-strip.stories.tsx @@ -64,7 +64,7 @@ function report(input: Partial): CapabilityAud declaredToolKindCount: 0, scheduledTaskCount: 0, enabledScheduledTaskCount: 0, - executableScheduledTaskCount: 0, + activeScheduledTaskCount: 0, failedScheduledTaskCount: 0, skippedScheduledTaskCount: 0, ...input, @@ -123,7 +123,7 @@ export const WithRisks: Story = { declaredToolKindCount: 5, scheduledTaskCount: 6, enabledScheduledTaskCount: 5, - executableScheduledTaskCount: 4, + activeScheduledTaskCount: 4, failedScheduledTaskCount: 1, skippedScheduledTaskCount: 1, })}