blob: 0232597ef8fc03ab7deee0a8f437e5f163c92283 [file]
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
composeSkillInvocationMessage,
listInvocableSkills,
prepareSkillInvocationMessage,
resolveSkillInvocations,
} from '../skill-invocation.js';
import {
loadSkillInstructions,
resolveSkillDiscoveryPaths,
writeSkillRuntimeState,
type HostCapabilities,
type LoadedSkillInstructions,
} from '../skills.js';
import { SKILL_INVOCATION_NAME_MAX_BYTES } from '@maka/core';
import { skillInvocationInlineReferences } from '../skill-invocation-receipt.js';
describe('skill invocation', () => {
it('projects successful receipt occurrences by request or canonical Skill id', () => {
const displayText = 'Use /skill:writer-id, not /skill:missing';
assert.deepEqual(
skillInvocationInlineReferences(
[
{
invocation: 'explicit',
request: 'workspace:skills:writer-id',
success: true,
ref: 'workspace:skills:writer-id',
id: 'writer-id',
name: 'Writer',
scope: 'workspace',
source: 'maka',
truncated: false,
},
{
invocation: 'explicit',
request: 'missing',
success: false,
reason: 'not_found',
},
{
invocation: 'model_tool',
request: 'researcher',
success: true,
ref: 'workspace:skills:researcher',
id: 'researcher',
name: 'Researcher',
scope: 'workspace',
source: 'maka',
truncated: false,
},
],
displayText,
),
[
{
kind: 'skill',
value: '/skill:writer-id',
label: 'Writer',
start: displayText.indexOf('/skill:writer-id'),
},
],
);
});
it('bounds successful Skill references to canonical transcript limits', () => {
const receipts = Array.from({ length: 40 }, (_, index) => ({
invocation: 'explicit' as const,
request: `skill-${index}`,
success: true as const,
ref: `project:agents:skill-${index}`,
id: `skill-${index}`,
name: `${'x'.repeat(199)}😀tail`,
scope: 'project' as const,
source: 'agents' as const,
truncated: false,
}));
const references = skillInvocationInlineReferences(
receipts,
receipts.map((receipt) => `/skill:${receipt.id}`).join(' '),
);
assert.equal(references.length, 32);
assert.equal(references[0]?.label.length, 199);
assert.equal(references[0]?.label.endsWith('\ud83d'), false);
});
it('lists only enabled, host-eligible skills as slim entries', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'plain-helper',
`---
name: Plain Helper
description: Helps with plain things.
---
# Plain Helper
Do plain things.`,
);
await writeSkill(
workspaceRoot,
'gated-helper',
`---
name: Gated Helper
description: Needs a host-specific tool.
required-tools: [ImaginaryTool]
---
# Gated Helper
Do gated things.`,
);
await writeSkill(
workspaceRoot,
'off-helper',
`---
name: Off Helper
description: Disabled by workspace state.
---
# Off Helper`,
);
await writeSkillRuntimeState(workspaceRoot, new Map([['off-helper', false]]));
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
const all = await listInvocableSkills(source);
assert.deepEqual(
all.map((skill) => skill.id).sort(),
['gated-helper', 'plain-helper'],
'disabled skills are not invocable',
);
assert.deepEqual(
Object.keys(all[0] ?? {}).sort(),
['description', 'id', 'name', 'ref'],
'slim entries only',
);
const host: HostCapabilities = { toolNames: new Set(['Read', 'Write']) };
const gated = await listInvocableSkills(source, host);
assert.deepEqual(
gated.map((skill) => skill.id),
['plain-helper'],
'required-tools mismatch is hidden',
);
const gatedHost: HostCapabilities = { toolNames: new Set(['Read', 'ImaginaryTool']) };
const eligible = await listInvocableSkills(source, gatedHost);
assert.deepEqual(eligible.map((skill) => skill.id).sort(), ['gated-helper', 'plain-helper']);
});
});
it('discovers skills across all standard paths with first-found-wins dedupe', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
const projectDir = join(workspaceRoot, 'project');
await writeSkillAt(
projectDir,
'.agents',
'skills',
'project-skill',
`---
name: Project Skill
description: Project level.
---
# Project Skill`,
);
await writeSkillAt(
projectDir,
'.agents',
'skills',
'shadowed',
`---
name: Project Shadow
description: Project copy wins.
---
# Project Shadow`,
);
await writeSkill(
workspaceRoot,
'shadowed',
`---
name: Workspace Shadow
description: Workspace copy loses.
---
# Workspace Shadow`,
);
const source = resolveSkillDiscoveryPaths(projectDir, workspaceRoot, homeDir);
const listed = await listInvocableSkills(source);
const shadowed = listed.find((skill) => skill.id === 'shadowed');
assert.deepEqual(listed.map((skill) => skill.id).sort(), ['project-skill', 'shadowed']);
assert.equal(shadowed?.name, 'Project Shadow');
assert.equal(shadowed?.ref, 'project:agents:shadowed');
});
});
it('resolves several requests against one scan with per-request failures', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---
name: Alpha
description: First.
---
# Alpha
Alpha body.`,
);
await writeSkill(
workspaceRoot,
'beta',
`---
name: Beta
description: Second.
required-tools: [MissingTool]
---
# Beta
Beta body.`,
);
await writeSkill(
workspaceRoot,
'gamma',
`---
name: Gamma
description: Disabled.
---
# Gamma`,
);
await writeSkillRuntimeState(workspaceRoot, new Map([['gamma', false]]));
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
const host: HostCapabilities = { toolNames: new Set(['Read']) };
const resolved = await resolveSkillInvocations(source, host, [
'alpha',
'Beta',
'missing',
'gamma',
]);
assert.deepEqual(
resolved.map((entry) => entry.request),
['alpha', 'Beta', 'missing', 'gamma'],
);
const [alpha, beta, missing, gamma] = resolved.map((entry) => entry.result);
assert.equal(alpha.ok, true);
if (alpha.ok) {
assert.equal(alpha.skill.id, 'alpha');
assert.equal(alpha.skill.instructions, '# Alpha\nAlpha body.');
assert.equal(alpha.skill.relativePath, 'skills/alpha/SKILL.md');
}
assert.deepEqual(
{ ok: beta.ok, reason: !beta.ok ? beta.reason : undefined },
{ ok: false, reason: 'host_incompatible' },
'name match still hits the host gate',
);
assert.deepEqual(
{ ok: missing.ok, reason: !missing.ok ? missing.reason : undefined },
{ ok: false, reason: 'not_found' },
);
assert.deepEqual(
{ ok: gamma.ok, reason: !gamma.ok ? gamma.reason : undefined },
{ ok: false, reason: 'disabled' },
);
});
});
it('matches loadSkillInstructions one-for-one against the same scan', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---
name: Alpha
description: First.
---
# Alpha
Body.`,
);
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
const host: HostCapabilities = { toolNames: new Set(['Read']) };
const [batched] = await resolveSkillInvocations(source, host, ['alpha']);
const single = await loadSkillInstructions(source, 'alpha', host);
assert.deepEqual(batched.result, single);
});
});
it('composes trust-framed skill blocks followed by the user message', () => {
const text = composeSkillInvocationMessage({
userText: '帮我整理这周的进展',
skills: [
fakeLoadedSkill({
id: 'weekly-report',
name: '写周报',
instructions: '# 写周报\n按模板整理。',
}),
fakeLoadedSkill({
id: 'data<crunch>',
name: 'Data "Crunch"',
instructions: '# Data\nCrunch it.',
}),
],
});
const skillSectionEnd = text.indexOf('</invoked-skill>');
assert.ok(text.startsWith('The user explicitly invoked'), 'trust framing opens the message');
assert.match(text, /lower priority than system, developer, safety, and permission rules/);
assert.match(text, /do not call the Skill tool again for these skills/);
assert.match(
text,
/<invoked-skill id="weekly-report" name="写周报">\n# 写周报\n按模板整理。\n<\/invoked-skill>/,
);
assert.match(
text,
/<invoked-skill id="data_crunch_" name="Data _Crunch_">/,
'attributes are sanitized',
);
assert.ok(text.indexOf('data_crunch_') > skillSectionEnd - 400, 'block order is request order');
assert.ok(text.endsWith('<user-message>\n帮我整理这周的进展\n</user-message>'));
});
it('falls back to a directive when the user sent invocations only', () => {
const text = composeSkillInvocationMessage({
userText: ' ',
skills: [fakeLoadedSkill({ id: 'weekly-report', name: '写周报', instructions: '# 写周报' })],
});
assert.doesNotMatch(text, /<user-message>/);
assert.ok(
text.endsWith(
'The user provided no additional task text; follow the skill instructions above.',
),
);
});
it('preserves significant leading indentation in the user-message body', () => {
const text = composeSkillInvocationMessage({
userText: ' make target',
skills: [fakeLoadedSkill({ id: 'alpha', name: 'Alpha', instructions: '# A' })],
});
assert.ok(
text.endsWith('<user-message>\n make target\n</user-message>'),
`expected indented body preserved, got: ${text}`,
);
});
it('prepares mixed success and failure tokens from one latest scan', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---
name: Alpha
description: First.
---
# Alpha
Alpha body.`,
);
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
const prepared = await prepareSkillInvocationMessage({
text: '/skill:alpha /skill:missing 整理一下',
source,
host: { toolNames: new Set(['Read']) },
});
assert.equal(prepared.disposition, 'ready');
assert.deepEqual(prepared.skillInvocation.loaded, [{ id: 'alpha', name: 'Alpha' }]);
assert.deepEqual(prepared.skillInvocation.failed, [
{ request: 'missing', reason: 'not_found' },
]);
assert.deepEqual(prepared.skillInvocation.receipts, [
{
invocation: 'explicit',
request: 'alpha',
success: true,
ref: 'workspace:legacy:alpha',
id: 'alpha',
name: 'Alpha',
scope: 'workspace',
source: 'legacy',
truncated: false,
},
{
invocation: 'explicit',
request: 'missing',
success: false,
reason: 'not_found',
},
]);
assert.ok('sendText' in prepared);
assert.match(prepared.sendText, /<invoked-skill id="alpha" name="Alpha">/);
assert.ok(!prepared.sendText.includes('/skill:alpha'));
assert.ok(!prepared.sendText.includes('/skill:missing'));
assert.match(prepared.sendText, /<user-message>\n整理一下\n<\/user-message>/);
});
});
it('projects long Skill metadata into a transport-bounded receipt', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
const name = '😀'.repeat(100);
await writeSkill(
workspaceRoot,
'long-name',
`---\nname: ${name}\ndescription: Long display metadata.\n---\n# Long name`,
);
const prepared = await prepareSkillInvocationMessage({
text: '/skill:long-name run',
source: resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir),
});
assert.equal(prepared.disposition, 'ready');
assert.equal(
new TextEncoder().encode(prepared.skillInvocation.loaded[0]?.name).byteLength,
SKILL_INVOCATION_NAME_MAX_BYTES,
);
const receipt = prepared.skillInvocation.receipts[0];
assert.ok(receipt?.success);
assert.equal(receipt.name, prepared.skillInvocation.loaded[0]?.name);
assert.match(prepared.sendText, new RegExp(name));
});
});
it('reads the current state at send time and blocks when every invocation fails', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---
name: Alpha
description: First.
---
# Alpha`,
);
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
await writeSkillRuntimeState(workspaceRoot, new Map([['alpha', false]]));
const prepared = await prepareSkillInvocationMessage({
text: '/skill:alpha do it',
source,
});
assert.equal(prepared.disposition, 'blocked');
assert.deepEqual(prepared.skillInvocation.loaded, []);
assert.deepEqual(prepared.skillInvocation.failed, [{ request: 'alpha', reason: 'disabled' }]);
assert.deepEqual(prepared.skillInvocation.receipts, [
{
invocation: 'explicit',
request: 'alpha',
success: false,
reason: 'disabled',
},
]);
});
});
it('merges structured ids before text tokens and deduplicates by id', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---\nname: Alpha\ndescription: First.\n---\n# Alpha`,
);
await writeSkill(workspaceRoot, 'beta', `---\nname: Beta\ndescription: Second.\n---\n# Beta`);
const prepared = await prepareSkillInvocationMessage({
text: '/skill:alpha /skill:beta finish',
skillIds: ['workspace:legacy:beta', 'ALPHA'],
source: resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir),
});
assert.equal(prepared.disposition, 'ready');
assert.deepEqual(
prepared.skillInvocation.loaded.map((entry) => entry.id),
['beta', 'alpha'],
);
const firstReceipt = prepared.skillInvocation.receipts[0];
assert.ok(firstReceipt && 'request' in firstReceipt);
assert.equal(firstReceipt.request, 'workspace:legacy:beta');
assert.equal(firstReceipt.success ? firstReceipt.ref : undefined, 'workspace:legacy:beta');
assert.ok('sendText' in prepared);
assert.ok(!prepared.sendText.includes('/skill:'));
});
});
it('blocks with resolution_failed when the authoritative scan throws', async () => {
const prepared = await prepareSkillInvocationMessage({
text: '/skill:alpha finish',
source: { dirs: null, stateRoot: '/invalid' } as unknown as Parameters<
typeof prepareSkillInvocationMessage
>[0]['source'],
});
assert.deepEqual(prepared, {
disposition: 'blocked',
skillInvocation: {
loaded: [],
failed: [{ request: 'alpha', reason: 'resolution_failed' }],
receipts: [
{
invocation: 'explicit',
request: 'alpha',
success: false,
reason: 'resolution_failed',
},
],
},
});
});
it('bounds explicit invocation request diagnostics', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
const prepared = await prepareSkillInvocationMessage({
text: 'run',
skillIds: [`bad\u0000${'x'.repeat(600)}`],
source: resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir),
});
assert.equal(prepared.disposition, 'blocked');
assert.equal(prepared.skillInvocation.failed.length, 1);
assert.equal(prepared.skillInvocation.receipts.length, 1);
const failure = prepared.skillInvocation.failed[0];
assert.ok(failure && failure.reason !== 'too_many_requests');
assert.equal(failure.request.length, 512);
assert.doesNotMatch(failure.request, /[\u0000-\u001F\u007F]/);
});
});
it('fails closed instead of resolving a partial request set after distinct-request overflow', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
await writeSkill(
workspaceRoot,
'alpha',
`---\nname: Alpha\ndescription: First.\n---\n# Alpha`,
);
const text = [
'/skill:alpha',
...Array.from({ length: 50 }, (_, index) => `/skill:missing-${index}`),
'finish',
].join(' ');
const prepared = await prepareSkillInvocationMessage({
text,
source: resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir),
});
assert.deepEqual(prepared, {
disposition: 'blocked',
skillInvocation: {
loaded: [],
failed: [{ reason: 'too_many_requests', requestLimit: 50 }],
receipts: [
{
invocation: 'explicit',
success: false,
reason: 'too_many_requests',
requestLimit: 50,
},
],
},
});
assert.ok(!('sendText' in prepared));
});
});
it('applies the distinct-request limit across structured and text inputs', async () => {
await withWorkspace(async (workspaceRoot, homeDir) => {
const structured = Array.from({ length: 50 }, (_, index) => `missing-${index}`);
const source = resolveSkillDiscoveryPaths(workspaceRoot, workspaceRoot, homeDir);
const atLimit = await prepareSkillInvocationMessage({
text: '/skill:MISSING-0 run',
skillIds: structured,
source,
});
assert.equal(atLimit.disposition, 'blocked');
assert.equal(atLimit.skillInvocation.failed.length, 50);
assert.equal(
atLimit.skillInvocation.failed.some((failure) => failure.reason === 'too_many_requests'),
false,
);
const overflow = await prepareSkillInvocationMessage({
text: '/skill:extra run',
skillIds: structured,
source,
});
assert.deepEqual(overflow.skillInvocation.failed, [
{ reason: 'too_many_requests', requestLimit: 50 },
]);
});
});
});
function fakeLoadedSkill(overrides: Partial<LoadedSkillInstructions>): LoadedSkillInstructions {
return {
ref: 'workspace:legacy:skill-id',
id: 'skill-id',
name: 'Skill Name',
description: '',
scope: 'workspace',
source: 'legacy',
declaredTools: [],
relativePath: 'skills/skill-id/SKILL.md',
instructions: '# Skill',
truncated: false,
...overrides,
};
}
// Tests pass an isolated homeDir so real user-level skills (~/.maka, ~/.agents)
// on the dev machine never leak into discovery results.
async function withWorkspace(
fn: (workspaceRoot: string, homeDir: string) => Promise<void>,
): Promise<void> {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-runtime-skill-invocation-'));
const homeDir = await mkdtemp(join(tmpdir(), 'maka-runtime-skill-invocation-home-'));
try {
await fn(workspaceRoot, homeDir);
} finally {
await rm(workspaceRoot, { recursive: true, force: true });
await rm(homeDir, { recursive: true, force: true });
}
}
async function writeSkill(workspaceRoot: string, id: string, content: string): Promise<void> {
await writeSkillAt(workspaceRoot, 'skills', id, content);
}
async function writeSkillAt(root: string, ...segmentsAndContent: string[]): Promise<void> {
const content = segmentsAndContent[segmentsAndContent.length - 1];
const segments = segmentsAndContent.slice(0, -1);
const id = segments[segments.length - 1];
const dir = join(root, ...segments.slice(0, -1), id);
await mkdir(dir, { recursive: true });
await writeFile(join(dir, 'SKILL.md'), content, 'utf8');
}