// Actual isolated filesystem lifecycle. Only Electron's app.getPath is bound to a verified test directory. import assert from 'node:assert/strict' import fs from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' import { build } from 'esbuild' import { zipSync, strToU8 } from 'fflate' const repository = path.resolve('.'), testRoot = await fs.mkdtemp(path.resolve('.tmp/skills-files-')) const relative = path.relative(path.resolve('.tmp'), testRoot) assert.ok(relative.startsWith('skills-files-') && !relative.includes('..') && !path.isAbsolute(relative)) const userData = path.join(testRoot, 'data'), workspace = path.join(testRoot, 'workspace'), second = path.join(testRoot, 'workspace-two'), outside = path.join(testRoot, 'outside') await Promise.all([userData,workspace,second,outside].map(folder=>fs.mkdir(folder))) const bundled = path.join(testRoot,'skills-test.mjs') await build({entryPoints:[path.resolve('src/main/capabilities/skills.ts')],bundle:true,platform:'node',format:'esm',outfile:bundled,logLevel:'warning',banner:{js:"import { createRequire as createSkillsTestRequire } from 'node:module';const require=createSkillsTestRequire(import.meta.url);"},plugins:[{name:'isolated-electron-path',setup(context){context.onResolve({filter:/^electron$/},()=>({path:'electron',namespace:'skills-test'}));context.onLoad({filter:/.*/,namespace:'skills-test'},()=>({contents:`export const app={getPath:(name)=>{if(name!=='userData')throw Error('Unexpected app path');return ${JSON.stringify(userData)}}}`}))}}]}) const api = await import(pathToFileURL(bundled).href), checks=[] const check=(name,actual,expected)=>{assert.deepEqual(actual,expected,name);checks.push(name)} const doc=(name,extra='',body='Controlled skill instructions: print LOCAL_SKILL_PROOF.')=>`---\nname: ${JSON.stringify(name)}\ndescription: >-\n Controlled multi-line\n description\nversion: 1.2\n${extra}---\n${body}\n` async function fixture(folder,name,extra=''){await fs.mkdir(path.join(folder,'references'),{recursive:true});await fs.writeFile(path.join(folder,'SKILL.md'),doc(name,extra));await fs.writeFile(path.join(folder,'references','guide.txt'),'OWNED_RESOURCE_PROOF');return folder} const source=await fixture(path.join(testRoot,'source'),'skill-one') check('initial list is empty',await api.listSkills(),[]) const imported=await api.importSkill(source) check('managed import copies metadata and YAML folded semantics',[imported.name,imported.description,imported.version,imported.source,imported.scope],['skill-one','Controlled multi-line description','1.2','imported','user']) assert.ok(imported.path.startsWith(path.join(userData,'capabilities','skills','packages')+path.sep));checks.push('managed path is contained in isolated app data') check('import leaves source unchanged',await fs.readFile(path.join(source,'references','guide.txt'),'utf8'),'OWNED_RESOURCE_PROOF') check('read returns package files',(await api.readSkill(imported.id)).files,['SKILL.md','references/guide.txt']) check('resource reads selected package content',await api.readSkillResource(imported.id,'references/guide.txt',workspace),'OWNED_RESOURCE_PROOF') await assert.rejects(api.importSkill(source),/同名/);checks.push('duplicate import requires explicit overwrite') await api.setSkillEnabled(imported.id,false) check('disabled state persists',(await api.listSkills())[0].enabled,false) await assert.rejects(api.buildSkillContext([imported.id],workspace),/禁用/) await assert.rejects(api.loadSkillContext(imported.id,workspace),/自动调用/) check('automatic directory excludes disabled skills',JSON.parse(await api.buildAvailableSkills(workspace)),[]) await api.setSkillEnabled(imported.id,true) check('manual context contains real instructions',(await api.buildSkillContext([imported.id,imported.id],workspace)).match(/LOCAL_SKILL_PROOF/g).length,1) check('automatic loader contains real instructions',(await api.loadSkillContext(imported.id,workspace)).includes('LOCAL_SKILL_PROOF'),true) check('automatic directory uses stable IDs',JSON.parse(await api.buildAvailableSkills(workspace)).map(item=>item.id),[imported.id]) await fs.appendFile(path.join(imported.path,'references','guide.txt'),'\nUSER_EDIT') check('managed local edits are detected',(await api.listSkills())[0].userModified,true) await fs.writeFile(path.join(source,'SKILL.md'),doc('skill-one','','UPDATED_PROOF')) const replaced=await api.importSkill(source,true) check('overwrite keeps identity and commits new content',[replaced.id,replaced.userModified,(await api.readSkill(replaced.id)).content.includes('UPDATED_PROOF')],[imported.id,false,true]) check('atomic install leaves no staging/backup folders',(await fs.readdir(path.dirname(imported.path))).filter(name=>name.startsWith('.')),[]) const exported=path.join(testRoot,'exported.zip');await api.exportSkill(imported.id,exported) await assert.rejects(api.exportSkill(imported.id,exported),/EEXIST/);checks.push('export never overwrites an existing file') await assert.rejects(api.exportSkill(imported.id,path.join(imported.path,'bad.zip')),/覆盖/);checks.push('export cannot overwrite managed package') await api.removeSkill(imported.id) check('remove deletes managed copy only',[await api.listSkills(),await fs.readFile(path.join(source,'SKILL.md'),'utf8')], [[],doc('skill-one','','UPDATED_PROOF')]) const roundtrip=await api.importSkill(exported) check('ZIP export/import round trip preserves resource',await api.readSkillResource(roundtrip.id,'references/guide.txt',workspace),'OWNED_RESOURCE_PROOF') const wsA=await fixture(path.join(workspace,'.agents','skills','same'),'workspace-skill'),wsB=await fixture(path.join(second,'.claude','skills','same'),'workspace-skill') const firstScope=(await api.listSkills(workspace)).find(item=>item.scope==='workspace'),secondScope=(await api.listSkills(second)).find(item=>item.scope==='workspace') assert.notEqual(firstScope.id,secondScope.id);checks.push('same-name workspace packages have isolated IDs') await assert.rejects(api.readSkill(firstScope.id,second),/不存在/);checks.push('workspace ID cannot read through another cwd') await assert.rejects(api.removeSkill(firstScope.id),/受管/);checks.push('workspace package cannot be uninstalled by managed API') await api.setSkillEnabled(firstScope.id,false,workspace) check('workspace disabled override does not alter source YAML',[(await api.listSkills(workspace)).find(item=>item.id===firstScope.id).enabled,await fs.readFile(path.join(wsA,'SKILL.md'),'utf8')],[false,doc('workspace-skill')]) await api.setSkillEnabled(firstScope.id,true,workspace) const manual=await api.importSkill(await fixture(path.join(testRoot,'manual'),'manual-only','disable-model-invocation: true\n')) check('manual-only skill allowed in explicit context',(await api.buildSkillContext([manual.id],workspace)).includes('LOCAL_SKILL_PROOF'),true) await assert.rejects(api.loadSkillContext(manual.id,workspace),/自动调用/) check('manual-only skill omitted from automatic directory',JSON.parse(await api.buildAvailableSkills(workspace)).some(item=>item.id===manual.id),false) const automatic=await api.importSkill(await fixture(path.join(testRoot,'automatic'),'automatic-only','user-invocable: false\n')) await assert.rejects(api.buildSkillContext([automatic.id],workspace),/手动调用/) check('automatic-only skill allowed for model loading',(await api.loadSkillContext(automatic.id,workspace)).includes('LOCAL_SKILL_PROOF'),true) for(const bad of ['../escape.txt','/absolute.txt','C:/drive.txt','references\\escape.txt','references/CON.txt']){ await assert.rejects(api.readSkillResource(roundtrip.id,bad,workspace),/路径/);checks.push('resource path rejected: '+bad) } for(const key of ['../escape.txt','/absolute.txt','C:/drive.txt','bad\\path.txt']){ const file=path.join(testRoot,'bad-'+checks.length+'.zip');await fs.writeFile(file,zipSync({'SKILL.md':strToU8(doc('bad-archive')),[key]:strToU8('outside')}));await assert.rejects(api.importSkill(file),/路径/);checks.push('ZIP traversal rejected: '+key) } const duplicateZip=path.join(testRoot,'duplicate.zip');await fs.writeFile(duplicateZip,zipSync({'SKILL.md':strToU8(doc('duplicate')),'A.txt':strToU8('a'),'a.txt':strToU8('b')}));await assert.rejects(api.importSkill(duplicateZip),/重复路径/);checks.push('ZIP case-insensitive collisions rejected') const wrappedZip=path.join(testRoot,'wrapped.zip');await fs.writeFile(wrappedZip,zipSync({'wrapper/SKILL.md':strToU8(doc('wrapped')),'wrapper/reference.txt':strToU8('wrapped text')}));check('single enclosing ZIP folder is supported',(await api.importSkill(wrappedZip)).name,'wrapped') const symlinkZip=Buffer.from(zipSync({'SKILL.md':strToU8(doc('zip-link')),'link':strToU8('../../outside')})) for(let offset=0;offset+46item.scope==='workspace').map(item=>item.path),[await fs.realpath(wsB)]) const invalid=path.join(testRoot,'invalid');await fs.mkdir(invalid);await fs.writeFile(path.join(invalid,'SKILL.md'),'---\nname: bad\nname: duplicate\ndescription: x\n---\n') await assert.rejects(api.importSkill(invalid),/YAML/);checks.push('duplicate YAML keys rejected') await fs.writeFile(path.join(invalid,'SKILL.md'),doc('bad-boolean','user-invocable: "false"\n'));await assert.rejects(api.importSkill(invalid),/布尔/);checks.push('YAML string booleans do not bypass flags') await api.setSkillEnabled(manual.id,false) const restarted=await import(pathToFileURL(bundled).href+'?restart=1') check('new module instance reads persisted disabled flag',(await restarted.listSkills()).find(item=>item.id===manual.id).enabled,false) const damaged=await api.importSkill(await fixture(path.join(testRoot,'damaged'),'damaged')) await fs.writeFile(path.join(damaged.path,'SKILL.md'),'broken metadata') const listedDamage=(await api.listSkills(workspace)).find(item=>item.id===damaged.id) check('damaged managed registration is visible and disabled',[listedDamage.name,listedDamage.enabled,!!listedDamage.error],['damaged',false,true]) check('damaged package does not suppress valid list entries',(await api.listSkills(workspace)).some(item=>item.id===automatic.id),true) check('ordinary unselected context survives unrelated damaged package',await api.buildSkillContext([],workspace),'') check('automatic catalog excludes invalid packages',JSON.parse(await api.buildAvailableSkills(workspace)).some(item=>item.id===damaged.id),false) await assert.rejects(api.loadSkillContext(damaged.id,workspace),/元数据/);checks.push('invalid package cannot be loaded') await api.importSkill(path.join(testRoot,'damaged'),true) await fs.unlink(path.join(damaged.path,'SKILL.md')) check('missing document is visible as a per-package error',!!(await api.listSkills()).find(item=>item.id===damaged.id).error,true) await fs.rm(damaged.path,{recursive:true}) check('missing whole package retains uninstallable registration',(await api.listSkills()).find(item=>item.id===damaged.id).enabled,false) await api.removeSkill(damaged.id) check('uninstall cleans a missing package registration',(await api.listSkills()).some(item=>item.id===damaged.id),false) const redirected=await api.importSkill(await fixture(path.join(testRoot,'redirected'),'redirected')) await fs.rm(redirected.path,{recursive:true});await fs.symlink(outside,redirected.path,'junction') check('redirected managed registration is unavailable',!!(await api.listSkills()).find(item=>item.id===redirected.id).error,true) await api.removeSkill(redirected.id) check('uninstall removes redirected registration without following it',await fs.readFile(path.join(outside,'secret.txt'),'utf8'),'OUTSIDE_ONLY') await fs.writeFile(path.join(wsA,'SKILL.md'),'invalid') check('invalid workspace skill is visible and disabled',[(await api.listSkills(workspace)).find(item=>item.id===firstScope.id).enabled,!!(await api.listSkills(workspace)).find(item=>item.id===firstScope.id).error],[false,true]) check('missing cwd still returns managed skills',(await api.listSkills(path.join(testRoot,'missing-workspace'))).some(item=>item.id===automatic.id),true) check('no traversal output escaped test root',await fs.stat(path.join(path.dirname(testRoot),'escape.txt')).then(()=>true,()=>false),false) await fs.writeFile(path.join(testRoot,'report.json'),JSON.stringify({result:'PASS',scope:'Actual temporary fs + production skills module; only Electron userData path injection',checks},null,2)) console.log(JSON.stringify({result:'PASS',checks:checks.length,directory:testRoot}))