🧭 feat: Scope Model Spec Skills (#13522)

* feat: scope model spec skills

* style: format skill catalog limit

* fix: serialize model spec skill resolution

* test: satisfy model spec load config typing

* fix: apply model spec skills to added conversations

* fix: support alwaysApply frontmatter alias

* fix: address model spec skills review
This commit is contained in:
Danny Avila 2026-06-05 10:22:02 -04:00 committed by GitHub
parent 3fb48021f7
commit 6357ea10c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1398 additions and 139 deletions

View file

@ -25,3 +25,27 @@ endpoints:
fetch: false
titleConvo: false
modelDisplayLabel: 'Mock Provider B'
modelSpecs:
list:
- name: 'e2e-mock-provider-a'
label: 'Mock Provider A'
preset:
endpoint: 'Mock Provider A'
model: 'mock-model-a'
- name: 'e2e-mock-provider-b'
label: 'Mock Provider B'
preset:
endpoint: 'Mock Provider B'
model: 'mock-model-b'
- name: 'e2e-skill-scope'
label: 'E2E Skill Scope'
preset:
endpoint: 'Mock Provider A'
model: 'mock-model-a'
skills:
- 'e2e-model-spec-allowed'
- 'e2e-model-spec-missing'
- 'e2e-model-spec-inaccessible'

View file

@ -14,13 +14,20 @@ const CHUNK_DELAY_MS = Number(process.env.MOCK_LLM_CHUNK_DELAY_MS) || 10;
const CREATE_SKILL_MARKER = 'E2E_CREATE_SKILL:';
const EDIT_SKILL_MARKER = 'E2E_EDIT_SKILL:';
const ASSERT_MODEL_SPEC_SKILLS_MARKER = 'E2E_ASSERT_MODEL_SPEC_SKILLS';
const CREATE_FILE_AUTHORING_FINAL_TEXT = 'E2E file authoring complete';
const EDIT_FILE_AUTHORING_FINAL_TEXT = 'E2E file edit complete';
const MODEL_SPEC_SKILL_ASSERTION_FINAL_TEXT = 'E2E model spec skill assertion passed';
const CREATE_FILE_TOOL_NAME = 'create_file';
const EDIT_FILE_TOOL_NAME = 'edit_file';
const BASH_TOOL_NAME = 'bash_tool';
const SKILL_TOOL_NAME = 'skill';
const CREATE_SKILL_TOOL_CALL_ID = 'call_e2e_create_skill';
const EDIT_SKILL_TOOL_CALL_ID = 'call_e2e_edit_skill';
const MODEL_SPEC_ACCESSIBLE_SKILL = 'e2e-model-spec-allowed';
const MODEL_SPEC_MISSING_SKILL = 'e2e-model-spec-missing';
const MODEL_SPEC_INACCESSIBLE_SKILL = 'e2e-model-spec-inaccessible';
const ALWAYS_APPLY_BODY_MARKER = 'E2E_ALWAYS_APPLY_BODY_MARKER';
const SKILL_DESCRIPTION =
'Use this skill to verify LibreChat skill file authoring in mock end-to-end tests.';
const EDITED_SKILL_DESCRIPTION =
@ -108,6 +115,67 @@ function collectToolNames(agents) {
return names;
}
function collectAdditionalInstructions(agents) {
return (agents ?? [])
.map((agent) =>
typeof agent?.additional_instructions === 'string' ? agent.additional_instructions : '',
)
.filter(Boolean)
.join('\n');
}
function collectSkillPrimeMessages(messages) {
return (messages ?? [])
.filter((message) => message?.additional_kwargs?.source === 'skill')
.map((message) => ({
name: message.additional_kwargs.skillName,
trigger: message.additional_kwargs.trigger,
content: getContentText(message.content),
}));
}
function modelSpecSkillAssertionResponses({ agents, messages, toolNames }) {
const failures = [];
const additionalInstructions = collectAdditionalInstructions(agents);
const skillPrimeMessages = collectSkillPrimeMessages(messages);
const alwaysApplyPrime = skillPrimeMessages.find(
(message) => message.name === MODEL_SPEC_ACCESSIBLE_SKILL && message.trigger === 'always-apply',
);
if (!toolNames.has(SKILL_TOOL_NAME)) {
failures.push(`${SKILL_TOOL_NAME} tool was not advertised`);
}
if (!additionalInstructions.includes(MODEL_SPEC_ACCESSIBLE_SKILL)) {
failures.push(`${MODEL_SPEC_ACCESSIBLE_SKILL} was not present in the model-visible catalog`);
}
if (additionalInstructions.includes(MODEL_SPEC_MISSING_SKILL)) {
failures.push(`${MODEL_SPEC_MISSING_SKILL} leaked into the model-visible catalog`);
}
if (additionalInstructions.includes(MODEL_SPEC_INACCESSIBLE_SKILL)) {
failures.push(`${MODEL_SPEC_INACCESSIBLE_SKILL} leaked into the model-visible catalog`);
}
if (!alwaysApplyPrime) {
failures.push(`${MODEL_SPEC_ACCESSIBLE_SKILL} was not always-apply primed`);
} else if (!alwaysApplyPrime.content.includes(ALWAYS_APPLY_BODY_MARKER)) {
failures.push(`${MODEL_SPEC_ACCESSIBLE_SKILL} always-apply body was missing its marker`);
}
if (skillPrimeMessages.some((message) => message.name === MODEL_SPEC_MISSING_SKILL)) {
failures.push(`${MODEL_SPEC_MISSING_SKILL} was unexpectedly primed`);
}
if (skillPrimeMessages.some((message) => message.name === MODEL_SPEC_INACCESSIBLE_SKILL)) {
failures.push(`${MODEL_SPEC_INACCESSIBLE_SKILL} was unexpectedly primed`);
}
if (failures.length > 0) {
return {
responses: [`E2E model spec skill assertion failed: ${failures.join('; ')}`],
};
}
return {
responses: [`${MODEL_SPEC_SKILL_ASSERTION_FINAL_TEXT}: ${MODEL_SPEC_ACCESSIBLE_SKILL}`],
};
}
function buildSkillBody(skillName) {
return `---
name: ${skillName}
@ -166,7 +234,11 @@ function fileAuthoringResponses(operation, toolNames) {
};
}
function resolveResponses(text, toolNames) {
function resolveResponses({ agents, messages, text, toolNames }) {
if (text.includes(ASSERT_MODEL_SPEC_SKILLS_MARKER)) {
return modelSpecSkillAssertionResponses({ agents, messages, toolNames });
}
const createSkillName = getRequestedSkillName(text, CREATE_SKILL_MARKER);
if (createSkillName) {
return fileAuthoringResponses(
@ -208,6 +280,11 @@ module.exports = function fakeModelHook(run, context) {
const text = getLatestUserText(context?.messages);
const toolNames = collectToolNames(context?.agents);
const { responses, toolCalls } = resolveResponses(text, toolNames);
const { responses, toolCalls } = resolveResponses({
agents: context?.agents,
messages: context?.messages,
text,
toolNames,
});
graph.overrideTestModel(responses, CHUNK_DELAY_MS, toolCalls);
};

View file

@ -14,6 +14,10 @@ export type MockEndpoint = (typeof MOCK_ENDPOINTS)[number];
export const NEW_CHAT_PATH = '/c/new';
type RefreshTokenBody = {
token?: string;
};
export function isAgentsStream(response: Response) {
return response.url().includes('/api/agents') && response.status() === 200;
}
@ -21,12 +25,30 @@ export function isAgentsStream(response: Response) {
const modelSelectorTrigger = (page: Page) =>
page.getByRole('button', { name: 'Select a model' }).first();
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/** Open the model selector, choose an endpoint, then its model (committed on the model click). */
export async function selectMockEndpoint(page: Page, endpoint: MockEndpoint) {
await modelSelectorTrigger(page).click();
const trigger = modelSelectorTrigger(page);
await trigger.click();
await page.getByRole('option', { name: endpoint.label }).click();
await page.getByRole('option', { name: endpoint.model, exact: true }).click();
await expect(modelSelectorTrigger(page)).not.toHaveText('Select a model');
const modelOption = page.getByRole('option', { name: endpoint.model, exact: true });
if (await modelOption.isVisible({ timeout: 1000 }).catch(() => false)) {
await modelOption.click();
}
await expect(trigger).not.toHaveText('Select a model');
}
/** Open the model selector and choose a configured model spec by label. */
export async function selectModelSpec(page: Page, label: string) {
const trigger = modelSelectorTrigger(page);
await expect(trigger).toBeVisible();
if ((await trigger.textContent())?.includes(label)) {
return;
}
await trigger.click();
await page.getByRole('option', { name: new RegExp(`^${escapeRegExp(label)}\\b`) }).click();
await expect(trigger).toContainText(label);
}
/** Enable the ephemeral Skills capability from the composer tool menu. */
@ -53,3 +75,88 @@ export async function sendMessage(page: Page, text: string): Promise<Response> {
]);
return response;
}
export async function getAccessToken(page: Page): Promise<string> {
const result = await page.evaluate(async () => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
});
if (!result.ok) {
throw new Error(
`Expected /api/auth/refresh to return 2xx, got ${result.status}: ${result.text}`,
);
}
const body = result.json as RefreshTokenBody | null;
if (!body?.token) {
throw new Error(`Expected /api/auth/refresh to return a token, got: ${result.text}`);
}
return body.token;
}
export async function requestJson<T>(
page: Page,
params: {
path: string;
token: string;
method?: string;
body?: unknown;
},
): Promise<T> {
const result = await page.evaluate(
async ({ accessToken, body, method, urlPath }) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
};
const init: RequestInit = {
method,
credentials: 'include',
headers,
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(body);
}
const response = await fetch(urlPath, init);
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
},
{
accessToken: params.token,
body: params.body,
method: params.method ?? 'GET',
urlPath: params.path,
},
);
if (!result.ok) {
throw new Error(
`Expected ${params.method ?? 'GET'} ${params.path} to return 2xx, got ${result.status}: ${result.text}`,
);
}
return result.json as T;
}
export async function fetchJson<T>(page: Page, path: string, token: string): Promise<T> {
return requestJson<T>(page, { path, token });
}

View file

@ -0,0 +1,175 @@
import { expect, test } from '@playwright/test';
import { MongoClient, ObjectId } from 'mongodb';
import type { Page } from '@playwright/test';
import { applyRuntimeEnv } from '../../setup/runtimeEnv';
import {
NEW_CHAT_PATH,
fetchJson,
getAccessToken,
requestJson,
selectModelSpec,
sendMessage,
} from './helpers';
const MODEL_SPEC_LABEL = 'E2E Skill Scope';
const ASSERTION_MARKER = 'E2E_ASSERT_MODEL_SPEC_SKILLS';
const ASSERTION_FINAL_TEXT = 'E2E model spec skill assertion passed';
const ACCESSIBLE_SKILL_NAME = 'e2e-model-spec-allowed';
const INACCESSIBLE_SKILL_NAME = 'e2e-model-spec-inaccessible';
const ALWAYS_APPLY_BODY_MARKER = 'E2E_ALWAYS_APPLY_BODY_MARKER';
const INACCESSIBLE_AUTHOR_ID = new ObjectId('64f000000000000000000001');
const SKILL_DESCRIPTION =
'Use this skill to verify model-spec skill scoping and always-apply priming in mock e2e tests.';
type SkillSummary = {
_id: string;
name: string;
description: string;
version: number;
alwaysApply?: boolean;
};
type SkillDetail = SkillSummary & {
body: string;
};
function buildSkillBody(name: string) {
return `---
name: ${name}
description: ${SKILL_DESCRIPTION}
alwaysApply: true
---
# ${name}
${ALWAYS_APPLY_BODY_MARKER}
This body should be injected as an always-apply skill prime.`;
}
async function findSkill(
page: Page,
skillName: string,
token: string,
): Promise<SkillSummary | null> {
const body = await fetchJson<{ skills?: SkillSummary[] }>(
page,
`/api/skills?search=${encodeURIComponent(skillName)}&limit=10`,
token,
);
return body.skills?.find((skill) => skill.name === skillName) ?? null;
}
async function seedAccessibleSkill(page: Page, token: string): Promise<SkillDetail> {
const body = buildSkillBody(ACCESSIBLE_SKILL_NAME);
const payload = {
name: ACCESSIBLE_SKILL_NAME,
description: SKILL_DESCRIPTION,
body,
};
const existing = await findSkill(page, ACCESSIBLE_SKILL_NAME, token);
if (!existing) {
return requestJson<SkillDetail>(page, {
path: '/api/skills',
token,
method: 'POST',
body: payload,
});
}
const detail = await fetchJson<SkillDetail>(
page,
`/api/skills/${encodeURIComponent(existing._id)}`,
token,
);
return requestJson<SkillDetail>(page, {
path: `/api/skills/${encodeURIComponent(existing._id)}`,
token,
method: 'PATCH',
body: {
...payload,
expectedVersion: detail.version,
},
});
}
async function seedInaccessibleSkill() {
applyRuntimeEnv();
if (!process.env.MONGO_URI) {
throw new Error('MONGO_URI must be available for model-spec skill mock e2e tests');
}
const client = new MongoClient(process.env.MONGO_URI);
await client.connect();
try {
const db = client.db();
const skills = db.collection('skills');
const aclEntries = db.collection('aclentries');
const now = new Date();
await skills.updateOne(
{
name: INACCESSIBLE_SKILL_NAME,
author: INACCESSIBLE_AUTHOR_ID,
tenantId: null,
},
{
$set: {
description: SKILL_DESCRIPTION,
body: buildSkillBody(INACCESSIBLE_SKILL_NAME),
authorName: 'Inaccessible E2E User',
updatedAt: now,
},
$setOnInsert: {
displayTitle: INACCESSIBLE_SKILL_NAME,
frontmatter: {},
disableModelInvocation: false,
userInvocable: true,
allowedTools: [],
alwaysApply: true,
source: 'inline',
fileCount: 0,
version: 1,
createdAt: now,
},
},
{ upsert: true },
);
const skill = await skills.findOne({
name: INACCESSIBLE_SKILL_NAME,
author: INACCESSIBLE_AUTHOR_ID,
tenantId: null,
});
if (skill?._id) {
await aclEntries.deleteMany({ resourceType: 'skill', resourceId: skill._id });
}
} finally {
await client.close();
}
}
test.describe('model spec skills', () => {
test('loads accessible configured skills and skips missing or inaccessible names', async ({
page,
}) => {
test.setTimeout(120000);
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
const token = await getAccessToken(page);
const skill = await seedAccessibleSkill(page, token);
expect(skill.alwaysApply).toBe(true);
await seedInaccessibleSkill();
await selectModelSpec(page, MODEL_SPEC_LABEL);
const response = await sendMessage(
page,
`${ASSERTION_MARKER}\nVerify model-spec skill scope and always-apply frontmatter.`,
);
expect(response.ok()).toBeTruthy();
await expect(
page
.getByTestId('messages-view')
.getByText(`${ASSERTION_FINAL_TEXT}: ${ACCESSIBLE_SKILL_NAME}`),
).toBeVisible({ timeout: 30000 });
});
});

View file

@ -4,6 +4,8 @@ import {
MOCK_ENDPOINTS,
NEW_CHAT_PATH,
enableSkills,
fetchJson,
getAccessToken,
selectMockEndpoint,
sendMessage,
} from './helpers';
@ -25,69 +27,8 @@ type SkillDetail = SkillSummary & {
body: string;
};
type RefreshTokenBody = {
token?: string;
};
const uniqueSkillName = () => `e2e-file-authoring-${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
async function getAccessToken(page: Page): Promise<string> {
const result = await page.evaluate(async () => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
});
if (!result.ok) {
throw new Error(
`Expected /api/auth/refresh to return 2xx, got ${result.status}: ${result.text}`,
);
}
const body = result.json as RefreshTokenBody | null;
if (!body?.token) {
throw new Error(`Expected /api/auth/refresh to return a token, got: ${result.text}`);
}
return body.token;
}
async function fetchJson<T>(page: Page, path: string, token: string): Promise<T> {
const result = await page.evaluate(
async ({ accessToken, urlPath }) => {
const response = await fetch(urlPath, {
credentials: 'include',
headers: { Authorization: `Bearer ${accessToken}` },
});
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
},
{ accessToken: token, urlPath: path },
);
if (!result.ok) {
throw new Error(`Expected ${path} to return 2xx, got ${result.status}: ${result.text}`);
}
return result.json as T;
}
async function findSkill(
page: Page,
skillName: string,