diff --git a/layers/identity/server/utils/digest-select.ts b/layers/identity/server/utils/digest-select.ts index e5f0a898..627dd886 100644 --- a/layers/identity/server/utils/digest-select.ts +++ b/layers/identity/server/utils/digest-select.ts @@ -75,6 +75,7 @@ export async function selectDigestForUser( WHERE a.id > ?2 AND a.id <= ?3 AND (sub.muted_until IS NULL OR sub.muted_until <= ?4) AND r.repo_kind != 'aggregator' + AND s.source_resolved = 1 AND s.current_sha IS NOT NULL AND TRIM(s.current_sha) != '' AND s.rendered_skill_path IS NOT NULL @@ -105,6 +106,7 @@ export async function selectDigestForUser( AND a.repo = ?3 AND a.id > ?4 AND a.id <= ?5 + AND s.source_resolved = 1 AND s.current_sha IS NOT NULL AND TRIM(s.current_sha) != '' AND s.rendered_skill_path IS NOT NULL @@ -120,6 +122,7 @@ export async function selectDigestForUser( AND a.repo = ?2 AND a.id > ?3 AND a.id <= ?4 + AND s.source_resolved = 1 AND s.current_sha IS NOT NULL AND TRIM(s.current_sha) != '' AND s.rendered_skill_path IS NOT NULL diff --git a/test/unit/digest-delivery.test.ts b/test/unit/digest-delivery.test.ts index 3ec6201b..4a45a7c4 100644 --- a/test/unit/digest-delivery.test.ts +++ b/test/unit/digest-delivery.test.ts @@ -623,6 +623,7 @@ const TEST_SCHEMA = ` description TEXT, current_sha TEXT, rendered_skill_path TEXT, + source_resolved INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (owner, repo, name) ); CREATE TABLE repos ( diff --git a/test/unit/digest-select.test.ts b/test/unit/digest-select.test.ts index f47fa757..fc8da68d 100644 --- a/test/unit/digest-select.test.ts +++ b/test/unit/digest-select.test.ts @@ -24,7 +24,8 @@ describe('digest selection', () => { name TEXT, description TEXT, current_sha TEXT, - rendered_skill_path TEXT + rendered_skill_path TEXT, + source_resolved INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE repos ( owner TEXT, @@ -57,7 +58,7 @@ describe('digest selection', () => { PRIMARY KEY (owner, repo, name, sha) ); - INSERT INTO skills VALUES ('nuxt', 'nuxt', 'nuxt', 'Nuxt framework', 'blob-new', 'skills/nuxt/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'nuxt', 'nuxt', 'Nuxt framework', 'blob-new', 'skills/nuxt/SKILL.md'); INSERT INTO repos VALUES ('nuxt', 'nuxt', 'source', 'main'); INSERT INTO skill_subscriptions VALUES (1, 'nuxt', 'nuxt', 'manual', NULL); INSERT INTO activity VALUES (1, 'nuxt', 'nuxt', 'nuxt', 500, 1500, 'blob-old'); @@ -102,7 +103,7 @@ describe('digest selection', () => { it('retains sorted skill names and per-skill counts for a multi-skill repo', async () => { sqlite.exec(` - INSERT INTO skills VALUES ('nuxt', 'nuxt', 'zeta', 'Zeta helper', 'blob-z2', 'skills/zeta/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'nuxt', 'zeta', 'Zeta helper', 'blob-z2', 'skills/zeta/SKILL.md'); INSERT INTO activity VALUES (3, 'nuxt', 'nuxt', 'zeta', 100, 1700, 'blob-z1'); INSERT INTO activity VALUES (4, 'nuxt', 'nuxt', 'zeta', 101, 1701, 'blob-z2'); INSERT INTO skill_revisions VALUES ('nuxt', 'nuxt', 'zeta', 'commit-z1', 100, 'zeta first'); @@ -127,8 +128,8 @@ describe('digest selection', () => { it('narrows a like-sourced subscription to the skills actually liked', async () => { sqlite.exec(` - INSERT INTO skills VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); - INSERT INTO skills VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); INSERT INTO repos VALUES ('nuxt', 'ui', 'source', 'main'); INSERT INTO skill_subscriptions VALUES (2, 'nuxt', 'ui', 'like', NULL); INSERT INTO skill_likes VALUES (2, 'nuxt', 'ui', 'design-tokens', 1); @@ -153,8 +154,8 @@ describe('digest selection', () => { it('keeps whole-repo scope for a manually watched repo', async () => { sqlite.exec(` - INSERT INTO skills VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); - INSERT INTO skills VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); INSERT INTO repos VALUES ('nuxt', 'ui', 'source', 'main'); INSERT INTO skill_subscriptions VALUES (3, 'nuxt', 'ui', 'manual', NULL); INSERT INTO skill_likes VALUES (3, 'nuxt', 'ui', 'design-tokens', 1); @@ -176,8 +177,8 @@ describe('digest selection', () => { it('drops a like-sourced repo entirely when nothing liked in it changed', async () => { sqlite.exec(` - INSERT INTO skills VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); - INSERT INTO skills VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'design-tokens', 'Tokens', 'a', 'design-tokens/SKILL.md'); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'ui', 'motion', 'Motion', 'b', 'motion/SKILL.md'); INSERT INTO repos VALUES ('nuxt', 'ui', 'source', 'main'); INSERT INTO skill_subscriptions VALUES (4, 'nuxt', 'ui', 'like', NULL); INSERT INTO skill_likes VALUES (4, 'nuxt', 'ui', 'design-tokens', 1); @@ -214,9 +215,27 @@ describe('digest selection', () => { }) }) + it.each(['manual', 'like'])('omits removed sources from a %s watch', async (scope) => { + sqlite.prepare('UPDATE skill_subscriptions SET source = ?').run(scope) + sqlite.exec(` + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path, source_resolved) + VALUES ('nuxt', 'nuxt', 'removed', 'Removed Skill', 'old-blob', 'removed/SKILL.md', 0); + INSERT INTO activity VALUES (3, 'nuxt', 'nuxt', 'removed', 1600, 1700, 'old-blob'); + INSERT INTO skill_likes VALUES (1, 'nuxt', 'nuxt', 'nuxt', 1); + INSERT INTO skill_likes VALUES (1, 'nuxt', 'nuxt', 'removed', 1); + `) + + const selection = await selectDigestForUser(db, digestUser(), 2000, { cursorStart: 0, cursorEnd: 3 }) + + expect(selection?.entries[0]).toMatchObject({ skillNames: ['nuxt'], changeCount: 2 }) + expect(selection?.entries[0]?.skills.map(skill => skill.sourceUrl)).toEqual([ + 'https://github.com/nuxt/nuxt/blob/main/skills/nuxt/SKILL.md', + ]) + }) + it('omits a Skill when its exact source cannot be resolved', async () => { sqlite.exec(` - INSERT INTO skills VALUES ('nuxt', 'nuxt', 'missing-source', 'Missing source', NULL, NULL); + INSERT INTO skills (owner, repo, name, description, current_sha, rendered_skill_path) VALUES ('nuxt', 'nuxt', 'missing-source', 'Missing source', NULL, NULL); INSERT INTO activity VALUES (3, 'nuxt', 'nuxt', 'missing-source', 1600, 1700, 'missing'); `)