From 7d70fbacbf1614966110812bb7b41faed98901e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:50:13 +0000 Subject: [PATCH 1/2] Initial plan From 579996ac6c130700abc3883ed60f3719dac904c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:28:59 +0000 Subject: [PATCH 2/2] Fix flaky skill watch test by adding non-recursive watchers on skill subdirs In scan mode, skill files are two levels deep (skillsDir/skill-name/SKILL.md). Only a recursive @parcel/watcher on skillsDir was used, which can fail to deliver content-only change events on macOS CI VMs. Add an explicit non-recursive watcher on each discovered skill subdirectory (trigger = SKILL.md file URI) to ensure content changes are reliably detected. This mirrors how agents and discover-mode skill files are watched. Update the existing watcher installation test and add a new test for SKILL.md content change detection. Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../copilot/sessionCustomizationDiscovery.ts | 7 ++++ .../sessionCustomizationDiscovery.test.ts | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts b/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts index e7cbc7b5fb4eb9..fb4c24e24440f9 100644 --- a/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts +++ b/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts @@ -1010,6 +1010,13 @@ export class SessionCustomizationDiscovery extends Disposable { if (skillStat.isFile && !seen.has(skillFile)) { seen.add(skillFile); files.push({ uri: skillFile, etag: skillStat.etag }); + // Watch the skill subdirectory directly for SKILL.md content changes. + // The recursive watcher on rootUri is sufficient for structural changes + // (skill directories added/deleted), but on some platforms (e.g. macOS VMs) + // a recursive watcher may not reliably fire for content-only changes deep + // in the tree. Adding an explicit non-recursive watcher on each skill's + // subdirectory ensures SKILL.md modifications are always detected. + addWatch(watchRootUris, child.resource, false, skillFile); } } catch { // SKILL.md missing — skip this skill directory. diff --git a/src/vs/platform/agentHost/test/node/sessionCustomizationDiscovery.test.ts b/src/vs/platform/agentHost/test/node/sessionCustomizationDiscovery.test.ts index 81cb56235c12c0..fedc197491654e 100644 --- a/src/vs/platform/agentHost/test/node/sessionCustomizationDiscovery.test.ts +++ b/src/vs/platform/agentHost/test/node/sessionCustomizationDiscovery.test.ts @@ -435,12 +435,18 @@ suite('SessionCustomizationDiscovery', () => { assert.strictEqual(watched.get(URI.joinPath(workspace, '.claude').toString()), false); assert.strictEqual(watched.get(URI.joinPath(workspace, '.github', 'agents').toString()), false); assert.strictEqual(watched.get(URI.joinPath(workspace, '.github', 'skills').toString()), true); + // Each discovered skill subdirectory is also watched non-recursively so that + // content changes to SKILL.md are detected even when the recursive watcher + // on the skills root does not fire reliably (e.g. on macOS VMs). + assert.strictEqual(watched.get(URI.joinPath(workspace, '.github', 'skills', 'bar').toString()), false); assert.strictEqual(watched.get(URI.joinPath(workspace, '.github', 'instructions').toString()), true); assert.strictEqual(watched.get(URI.joinPath(workspace, '.github', 'hooks').toString()), true); assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot').toString()), false); assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot', 'agents').toString()), false); assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot', 'skills').toString()), true); + assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot', 'skills', 'copilot-user-skill').toString()), false); assert.strictEqual(watched.get(URI.joinPath(userHome, '.agents', 'skills').toString()), true); + assert.strictEqual(watched.get(URI.joinPath(userHome, '.agents', 'skills', 'user-skill').toString()), false); assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot', 'instructions').toString()), true); assert.strictEqual(watched.get(URI.joinPath(userHome, '.copilot', 'hooks').toString()), true); }); @@ -518,6 +524,32 @@ suite('SessionCustomizationDiscovery', () => { assert.strictEqual(changeCount, 1, 'expected onDidChange to fire when an existing agent file is modified'); }); + test('fires onDidChange when an existing SKILL.md file is modified', async () => { + // Skill files live two levels deep (skillsDir/skill-name/SKILL.md), so a + // recursive watcher on the skills root is the primary detection mechanism. + // An explicit non-recursive watcher on each skill subdirectory is also added + // as a more reliable secondary trigger for content-only changes. + await seed('/workspace/.github/skills/my-skill/SKILL.md', 'skill body'); + + const discovery = disposables.add(instantiationService.createInstance(SessionCustomizationDiscovery, workspace, userHome, URI.file)); + await discovery.scan(CancellationToken.None); + await timeout(50); + + let changeCount = 0; + const fired = new DeferredPromise(); + disposables.add(discovery.onDidChange(() => { + changeCount++; + fired.complete(); + })); + + // Overwrite the skill file to produce an UPDATED event via the non-recursive + // watcher on the skill subdirectory (`my-skill/`). + await seed('/workspace/.github/skills/my-skill/SKILL.md', 'skill body (updated)'); + await raceTimeout(fired.p, 500); + + assert.strictEqual(changeCount, 1, 'expected onDidChange to fire when an existing SKILL.md is modified'); + }); + test('fires onDidChange when an existing agent file is deleted under a non-recursively watched root', async () => { const agentUri = await seed('/workspace/.github/agents/foo.agent.md', 'workspace agent'); // Seed a second agent so the parent directory still exists after the deletion.