Use getRequestDirectory and x-opencode-directory like the other skill mutations, and pin renamable list/store mapping with focused tests. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
205 lines
6.7 KiB
TypeScript
205 lines
6.7 KiB
TypeScript
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
|
|
const activeProjectPath = '/workspace/project-with-agents-skills';
|
|
|
|
let runtimeFetchCalls: Array<{ url: string; headers?: HeadersInit }> = [];
|
|
let runtimeFetchImpl: (url: string, init?: RequestInit) => Promise<Response> = async () => (
|
|
new Response(JSON.stringify({ skills: [] }), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
);
|
|
let getDirectoryImpl: () => string | undefined = () => undefined;
|
|
|
|
const runtimeFetchMock = async (url: string, init?: RequestInit) => {
|
|
runtimeFetchCalls.push({ url: String(url), headers: init?.headers });
|
|
return runtimeFetchImpl(url, init);
|
|
};
|
|
|
|
mock.module('@/lib/opencode/client', () => ({
|
|
opencodeClient: {
|
|
getDirectory: () => getDirectoryImpl(),
|
|
checkHealth: async () => true,
|
|
},
|
|
}));
|
|
|
|
mock.module('@/stores/useProjectsStore', () => ({
|
|
useProjectsStore: {
|
|
getState: () => ({
|
|
getActiveProject: () => ({ path: activeProjectPath }),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
mock.module('@/lib/runtime-fetch', () => ({
|
|
runtimeFetch: runtimeFetchMock,
|
|
}));
|
|
|
|
mock.module('@/lib/background-network', () => ({
|
|
runBackgroundNetworkTask: async <T,>(task: () => Promise<T>) => task(),
|
|
}));
|
|
|
|
mock.module('@/lib/configUpdate', () => ({
|
|
startConfigUpdate: mock(() => undefined),
|
|
finishConfigUpdate: mock(() => undefined),
|
|
updateConfigUpdateMessage: mock(() => undefined),
|
|
}));
|
|
|
|
mock.module('@/lib/configSync', () => ({
|
|
emitConfigChange: mock(() => undefined),
|
|
scopeMatches: mock(() => false),
|
|
subscribeToConfigChanges: mock(() => () => undefined),
|
|
}));
|
|
|
|
mock.module('./utils/safeStorage', () => ({
|
|
createDeferredSafeJSONStorage: () => ({
|
|
getItem: async () => null,
|
|
setItem: async () => undefined,
|
|
removeItem: async () => undefined,
|
|
}),
|
|
}));
|
|
|
|
const { invalidateSkillsLoadCache, useSkillsStore } = await import('./useSkillsStore');
|
|
|
|
describe('useSkillsStore directory resolution', () => {
|
|
beforeEach(() => {
|
|
runtimeFetchCalls = [];
|
|
getDirectoryImpl = () => undefined;
|
|
runtimeFetchImpl = async () => new Response(JSON.stringify({
|
|
skills: [{
|
|
name: 'repo-local-skill',
|
|
path: `${activeProjectPath}/.agents/skills/repo-local-skill/SKILL.md`,
|
|
scope: 'project',
|
|
source: 'agents',
|
|
sources: { md: { description: 'Repository local' } },
|
|
}],
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
invalidateSkillsLoadCache(activeProjectPath);
|
|
useSkillsStore.setState({
|
|
selectedSkillName: null,
|
|
skills: [],
|
|
isLoading: false,
|
|
skillDraft: null,
|
|
});
|
|
});
|
|
|
|
test('loadSkills scopes discovery to the active project even when client directory is unset', async () => {
|
|
const loaded = await useSkillsStore.getState().loadSkills();
|
|
|
|
expect(loaded).toBe(true);
|
|
expect(runtimeFetchCalls.length).toBe(1);
|
|
expect(runtimeFetchCalls[0]?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
|
expect(useSkillsStore.getState().skills).toEqual([{
|
|
name: 'repo-local-skill',
|
|
path: `${activeProjectPath}/.agents/skills/repo-local-skill/SKILL.md`,
|
|
scope: 'project',
|
|
source: 'agents',
|
|
description: 'Repository local',
|
|
group: undefined,
|
|
renamable: false,
|
|
}]);
|
|
});
|
|
|
|
test('loadSkills maps authoritative renamable from the list response', async () => {
|
|
runtimeFetchImpl = async () => new Response(JSON.stringify({
|
|
skills: [
|
|
{
|
|
name: 'managed-skill',
|
|
path: `${activeProjectPath}/.opencode/skills/managed-skill/SKILL.md`,
|
|
scope: 'project',
|
|
source: 'opencode',
|
|
renamable: true,
|
|
sources: { md: { description: 'Managed' } },
|
|
},
|
|
{
|
|
name: 'cache-skill',
|
|
path: '/home/ubuntu/.cache/opencode/skills/hash/cache-skill/SKILL.md',
|
|
scope: 'user',
|
|
source: 'opencode',
|
|
renamable: false,
|
|
sources: { md: { description: 'Cache' } },
|
|
},
|
|
],
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
|
expect(useSkillsStore.getState().skills).toEqual([
|
|
{
|
|
name: 'managed-skill',
|
|
path: `${activeProjectPath}/.opencode/skills/managed-skill/SKILL.md`,
|
|
scope: 'project',
|
|
source: 'opencode',
|
|
description: 'Managed',
|
|
group: undefined,
|
|
renamable: true,
|
|
},
|
|
{
|
|
name: 'cache-skill',
|
|
path: '/home/ubuntu/.cache/opencode/skills/hash/cache-skill/SKILL.md',
|
|
scope: 'user',
|
|
source: 'opencode',
|
|
description: 'Cache',
|
|
group: 'hash',
|
|
renamable: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('renameSkill uses getRequestDirectory query and x-opencode-directory header', async () => {
|
|
runtimeFetchImpl = async (_url, init) => {
|
|
if (init?.method === 'PATCH') {
|
|
return new Response(JSON.stringify({
|
|
success: true,
|
|
requiresReload: false,
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
return new Response(JSON.stringify({
|
|
skills: [{
|
|
name: 'new-skill',
|
|
path: `${activeProjectPath}/.opencode/skills/new-skill/SKILL.md`,
|
|
scope: 'project',
|
|
source: 'opencode',
|
|
renamable: true,
|
|
sources: { md: { description: 'Renamed' } },
|
|
}],
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
};
|
|
|
|
const renamed = await useSkillsStore.getState().renameSkill('old-skill', 'new-skill');
|
|
expect(renamed).toBe(true);
|
|
|
|
const renameCall = runtimeFetchCalls.find((call) => String(call.url).includes('/api/config/skills/old-skill'));
|
|
expect(renameCall).toBeTruthy();
|
|
expect(renameCall?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
|
|
|
const headers = new Headers(renameCall?.headers);
|
|
expect(headers.get('content-type')).toBe('application/json');
|
|
expect(headers.get('x-opencode-directory')).toBe(activeProjectPath);
|
|
});
|
|
|
|
test('invalidateSkillsLoadCache() with no argument clears the active-project cache key used by loadSkills', async () => {
|
|
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
|
expect(runtimeFetchCalls.length).toBe(1);
|
|
|
|
// Wrong key: client-directory-first null maps to __default__, not the active project.
|
|
invalidateSkillsLoadCache(null);
|
|
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
|
expect(runtimeFetchCalls.length).toBe(1);
|
|
|
|
// Default resolution must match loadSkills (active project first).
|
|
invalidateSkillsLoadCache();
|
|
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
|
expect(runtimeFetchCalls.length).toBe(2);
|
|
expect(runtimeFetchCalls[1]?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
|
});
|
|
});
|