fix(skills): repair renameSkill directory resolution after merge
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>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
5b9a8c4bef
commit
0d24d0a167
@@ -98,9 +98,94 @@ describe('useSkillsStore directory resolution', () => {
|
|||||||
source: 'agents',
|
source: 'agents',
|
||||||
description: 'Repository local',
|
description: 'Repository local',
|
||||||
group: undefined,
|
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 () => {
|
test('invalidateSkillsLoadCache() with no argument clears the active-project cache key used by loadSkills', async () => {
|
||||||
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
||||||
expect(runtimeFetchCalls.length).toBe(1);
|
expect(runtimeFetchCalls.length).toBe(1);
|
||||||
|
|||||||
@@ -408,12 +408,15 @@ export const useSkillsStore = create<SkillsStore>()(
|
|||||||
startConfigUpdate("Renaming skill...");
|
startConfigUpdate("Renaming skill...");
|
||||||
let requiresReload = false;
|
let requiresReload = false;
|
||||||
try {
|
try {
|
||||||
const currentDirectory = getCurrentDirectory();
|
const directory = getRequestDirectory();
|
||||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||||
|
|
||||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`, {
|
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(directory ? { 'x-opencode-directory': directory } : {}),
|
||||||
|
},
|
||||||
body: JSON.stringify({ renameTo: newName }),
|
body: JSON.stringify({ renameTo: newName }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -424,7 +427,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const needsReload = payload?.requiresReload ?? false;
|
const needsReload = payload?.requiresReload ?? false;
|
||||||
invalidateSkillsLoadCache(currentDirectory);
|
invalidateSkillsLoadCache(directory);
|
||||||
if (needsReload) {
|
if (needsReload) {
|
||||||
requiresReload = true;
|
requiresReload = true;
|
||||||
await refreshSkillsAfterOpenCodeRestart({
|
await refreshSkillsAfterOpenCodeRestart({
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
deleteSkill,
|
deleteSkill,
|
||||||
discoverSkills,
|
discoverSkills,
|
||||||
getSkillSources,
|
getSkillSources,
|
||||||
|
isManagedSkillPath,
|
||||||
mergeDiscoveredSkills,
|
mergeDiscoveredSkills,
|
||||||
|
renameSkill,
|
||||||
updateSkill,
|
updateSkill,
|
||||||
} from './skills.js';
|
} from './skills.js';
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +60,8 @@ const startSkillsApp = ({ projectRoot }) => {
|
|||||||
createSkill,
|
createSkill,
|
||||||
updateSkill,
|
updateSkill,
|
||||||
deleteSkill,
|
deleteSkill,
|
||||||
|
renameSkill,
|
||||||
|
isManagedSkillPath,
|
||||||
readSkillSupportingFile,
|
readSkillSupportingFile,
|
||||||
writeSkillSupportingFile,
|
writeSkillSupportingFile,
|
||||||
deleteSkillSupportingFile,
|
deleteSkillSupportingFile,
|
||||||
@@ -154,4 +158,62 @@ describe('skill-routes directory soft fallback', () => {
|
|||||||
const payload = await listResponse.json();
|
const payload = await listResponse.json();
|
||||||
expect(payload.skills.map((skill) => skill.name)).toContain('manual-repo-skill');
|
expect(payload.skills.map((skill) => skill.name)).toContain('manual-repo-skill');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks managed-root skills renamable and cache skills not renamable', async () => {
|
||||||
|
projectRoot = createTempProject();
|
||||||
|
const managedDir = path.join(projectRoot, '.opencode', 'skills', 'managed-list-skill');
|
||||||
|
fs.mkdirSync(managedDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(managedDir, 'SKILL.md'),
|
||||||
|
[
|
||||||
|
'---',
|
||||||
|
'name: managed-list-skill',
|
||||||
|
'description: Managed list skill',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'Managed body',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const cacheStamp = `oc-skill-routes-${Date.now()}`;
|
||||||
|
const cacheDir = path.join(os.homedir(), '.cache', 'opencode', 'skills', cacheStamp, 'cache-list-skill');
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(cacheDir, 'SKILL.md'),
|
||||||
|
[
|
||||||
|
'---',
|
||||||
|
'name: cache-list-skill',
|
||||||
|
'description: Cache list skill',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'Cache body',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
appHandle = startSkillsApp({ projectRoot });
|
||||||
|
const listResponse = await fetch(
|
||||||
|
`${appHandle.baseUrl}/api/config/skills?directory=${encodeURIComponent(projectRoot)}`,
|
||||||
|
);
|
||||||
|
expect(listResponse.status).toBe(200);
|
||||||
|
const payload = await listResponse.json();
|
||||||
|
|
||||||
|
const managed = payload.skills.find((entry) => entry.name === 'managed-list-skill');
|
||||||
|
const cached = payload.skills.find((entry) => entry.name === 'cache-list-skill');
|
||||||
|
|
||||||
|
expect(managed).toBeTruthy();
|
||||||
|
expect(managed.renamable).toBe(true);
|
||||||
|
expect(cached).toBeTruthy();
|
||||||
|
expect(cached.renamable).toBe(false);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(path.join(os.homedir(), '.cache', 'opencode', 'skills', cacheStamp), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user