Merge branch 'codex/skills-symlink-scan'
This commit is contained in:
commit
80486d98eb
@ -4274,13 +4274,8 @@ class AppController extends ChangeNotifier {
|
||||
if (!await root.exists()) {
|
||||
continue;
|
||||
}
|
||||
await for (final entity in root.list(
|
||||
recursive: true,
|
||||
followLinks: false,
|
||||
)) {
|
||||
if (entity is! File || entity.uri.pathSegments.last != 'SKILL.md') {
|
||||
continue;
|
||||
}
|
||||
final skillFiles = await _collectSkillFilesFromDirectory(root);
|
||||
for (final entity in skillFiles) {
|
||||
final entry = await _skillEntryFromFile(
|
||||
entity,
|
||||
rootSpec,
|
||||
@ -4303,6 +4298,57 @@ class AppController extends ChangeNotifier {
|
||||
return entries;
|
||||
}
|
||||
|
||||
Future<List<File>> _collectSkillFilesFromDirectory(Directory root) async {
|
||||
final skillFiles = <File>[];
|
||||
final visitedDirectories = <String>{};
|
||||
|
||||
Future<void> visitDirectory(Directory directory) async {
|
||||
final directoryKey = await _directoryScanKey(directory);
|
||||
if (!visitedDirectories.add(directoryKey)) {
|
||||
return;
|
||||
}
|
||||
await for (final entity in directory.list(followLinks: false)) {
|
||||
if (entity is File) {
|
||||
if (entity.uri.pathSegments.last == 'SKILL.md') {
|
||||
skillFiles.add(entity);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entity is Directory) {
|
||||
await visitDirectory(entity);
|
||||
continue;
|
||||
}
|
||||
if (entity is! Link) {
|
||||
continue;
|
||||
}
|
||||
final resolvedType = await FileSystemEntity.type(
|
||||
entity.path,
|
||||
followLinks: true,
|
||||
);
|
||||
if (resolvedType == FileSystemEntityType.file) {
|
||||
if (entity.uri.pathSegments.last == 'SKILL.md') {
|
||||
skillFiles.add(File(entity.path));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (resolvedType == FileSystemEntityType.directory) {
|
||||
await visitDirectory(Directory(entity.path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await visitDirectory(root);
|
||||
return skillFiles;
|
||||
}
|
||||
|
||||
Future<String> _directoryScanKey(Directory directory) async {
|
||||
try {
|
||||
return await directory.resolveSymbolicLinks();
|
||||
} catch (_) {
|
||||
return directory.absolute.path;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AssistantThreadSkillEntry>> _scanSingleAgentSharedSkillEntries() {
|
||||
return _scanSingleAgentSkillEntries(_singleAgentSharedSkillScanRoots);
|
||||
}
|
||||
|
||||
@ -210,6 +210,61 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController scans skills inside symlinked directories under shared roots',
|
||||
() async {
|
||||
if (Platform.isWindows) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-skill-directory-symlink-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDirectory.exists()) {
|
||||
try {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
final sharedRoot = Directory('${tempDirectory.path}/shared-root');
|
||||
final actualSkillRoot = Directory('${tempDirectory.path}/actual-skills');
|
||||
await sharedRoot.create(recursive: true);
|
||||
await _writeSkill(
|
||||
actualSkillRoot,
|
||||
'linked-browser',
|
||||
skillName: 'Linked Browser',
|
||||
description: 'Loaded through a symlinked directory',
|
||||
);
|
||||
await Link('${sharedRoot.path}/linked-pack').create(actualSkillRoot.path);
|
||||
|
||||
final controller = AppController(
|
||||
store: await _createStore(tempDirectory.path),
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
singleAgentSharedSkillScanRootOverrides: <String>[sharedRoot.path],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
await _waitFor(
|
||||
() => controller
|
||||
.assistantImportedSkillsForSession(controller.currentSessionKey)
|
||||
.any((skill) => skill.label == 'Linked Browser'),
|
||||
);
|
||||
|
||||
final linkedSkill = controller
|
||||
.assistantImportedSkillsForSession(controller.currentSessionKey)
|
||||
.firstWhere((skill) => skill.label == 'Linked Browser');
|
||||
expect(linkedSkill.description, 'Loaded through a symlinked directory');
|
||||
expect(linkedSkill.source, 'custom');
|
||||
expect(linkedSkill.sourceLabel, contains('linked-pack/linked-browser'));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController resolves preset shared roots against the access service home directory',
|
||||
() async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user