Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion layers/registry/app/components/SkillDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2244,7 +2244,7 @@ useHead(computed(() => ({
role="region"
>
<SkillBehaviors
v-if="data.raw"
v-if="data.raw || behaviors.length"
:behaviors="behaviors"
/>
<SkillThirdPartyChecks
Expand Down
13 changes: 7 additions & 6 deletions layers/registry/server/api/skills/[...slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const skillDetailHandler = defineApiHandler({

return cached({
storage: useStorage('edge-cache'),
key: `skills:detail:v3:${slug.toLowerCase()}`,
key: `skills:detail:v4:${slug.toLowerCase()}`,
ttlSeconds: DETAIL_CACHE_TTL,
staleSeconds: DETAIL_CACHE_STALE_TTL,
compute: () => loadSkillDetail(event, platform, slug),
Expand Down Expand Up @@ -328,11 +328,6 @@ async function loadSkillDetail(event: H3Event, platform: Platform, slug: string)
}
const allowedTools = parseAllowedTools(rendered.frontmatter)
const capability = classifyAllowedTools(allowedTools)
const behaviors = skillPageBehaviors({
raw: rendered.raw,
assetPaths: assets.map(asset => asset.path),
source: { owner: source.owner, repo: source.repo, branch, skillPath: rendered.skillPath },
})
const selectedAssets = selectSkillFiles(assets)
// `rendered.*` describes the cached copy, which survives the file being
// deleted upstream, so it can only ever say "we can still render this". The
Expand All @@ -346,6 +341,12 @@ async function loadSkillDetail(event: H3Event, platform: Platform, slug: string)
raw: rendered.raw,
})
const sourceGone = !sourceResolved && row.source_resolved === 0
const behaviors = skillPageBehaviors({
raw: rendered.raw,
assetPaths: assets.map(asset => asset.path),
// A deleted upstream file has nothing to link to.
source: { owner: source.owner, repo: source.repo, branch, skillPath: sourceGone ? null : rendered.skillPath },
})
// `current_sha` is the blob sha of SKILL.md, not a commit, so it cannot pin a link.
const sourceCommitSha = row.latest_revision_sha ?? null
const pushedAtIso = epochToIso(row.pushed_at)
Expand Down
5 changes: 4 additions & 1 deletion layers/registry/server/utils/skill-behaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export function skillPageBehaviors(input: {
skillPath: directory ? `${directory}/${location.path}` : location.path,
branch: input.source.branch,
})
return file && location.line !== null ? `${file}#L${location.line}` : file
if (!file || location.line === null)
return file
// GitHub renders Markdown, and a line anchor only works on its source view.
return /\.(?:md|mdx|markdown)$/i.test(location.path) ? `${file}?plain=1#L${location.line}` : `${file}#L${location.line}`
}
return detectBehaviors(files).map(behavior => ({
...behavior,
Expand Down
49 changes: 35 additions & 14 deletions test/unit/skill-detail-behaviors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,40 @@ vi.stubGlobal('useStorage', () => ({

let harness: SqliteD1

beforeEach(() => {
vi.resetModules()
cacheMap.clear()
harness = createSqliteD1(allMigrations())
function seed(sourceResolved: number) {
harness.raw.prepare(`INSERT INTO repos (owner, repo, default_branch) VALUES ('acme', 'skills', 'main')`).run()
const raw = '---\nname: setup\ndescription: Set up the project.\n---\n\n```sh\nsudo apt-get install jq\n```\n'
harness.raw.prepare(
`INSERT INTO skills (owner, repo, name, slug, display_name, source_resolved,
rendered_skill_path, rendered_status, rendered_raw, rendered_html, rendered_at, assets)
VALUES ('acme', 'skills', 'setup', 'acme/skills/setup', 'setup', 1,
VALUES ('acme', 'skills', 'setup', 'acme/skills/setup', 'setup', ?,
'skills/setup/SKILL.md', 'ok', ?, '<p>ok</p>', ?, ?)`,
).run(raw, NOW_SEC, JSON.stringify([{ path: 'scripts/install.sh', size: 12, type: 'code' }]))
).run(sourceResolved, raw, NOW_SEC, JSON.stringify([{ path: 'scripts/install.sh', size: 12, type: 'code' }]))
}

interface Body {
sourceFacts: { behaviors: Array<{ id: string, tier: string, locations: Array<{ path: string, line: number | null, url: string | null }> }> }
}

async function detail(): Promise<Body> {
const handler = (await import('../../layers/registry/server/api/skills/[...slug].get')).default as (event: H3Event) => Promise<Body>
return handler({
context: { platform: { db: harness.db } },
node: { req: { headers: {} } },
} as unknown as H3Event)
}

beforeEach(() => {
vi.resetModules()
cacheMap.clear()
harness = createSqliteD1(allMigrations())
})

describe('skill detail behaviors', () => {
it('names each behavior in SKILL.md and the stored file names, with a link to where it appears', async () => {
const handler = (await import('../../layers/registry/server/api/skills/[...slug].get')).default as (event: H3Event) => Promise<{
sourceFacts: { behaviors: Array<{ id: string, tier: string, locations: Array<{ path: string, line: number | null, url: string | null }> }> }
}>
seed(1)

const body = await handler({
context: { platform: { db: harness.db } },
node: { req: { headers: {} } },
} as unknown as H3Event)
const body = await detail()

expect(body.sourceFacts.behaviors.map(behavior => [behavior.id, behavior.tier])).toEqual([
['privilege', 'ask'],
Expand All @@ -54,8 +64,19 @@ describe('skill detail behaviors', () => {
expect(body.sourceFacts.behaviors[0]?.locations).toEqual([{
path: 'SKILL.md',
line: 7,
url: expect.stringMatching(/^https:\/\/github\.com\/acme\/skills\/blob\/[^/]+\/skills\/setup\/SKILL\.md#L7$/),
url: expect.stringMatching(/^https:\/\/github\.com\/acme\/skills\/blob\/[^/]+\/skills\/setup\/SKILL\.md\?plain=1#L7$/),
}])
expect(body.sourceFacts.behaviors[2]?.locations[0]?.url).toMatch(/\/skills\/setup\/scripts\/install\.sh$/)
})

it('links nothing once the upstream SKILL.md is gone', async () => {
seed(0)

const body = await detail()

expect(body.sourceFacts.behaviors.length).toBeGreaterThan(0)
expect(body.sourceFacts.behaviors.flatMap(behavior => behavior.locations.map(location => location.url))).toEqual(
body.sourceFacts.behaviors.flatMap(behavior => behavior.locations.map(() => null)),
)
})
})
2 changes: 1 addition & 1 deletion test/unit/skill-detail-swr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { allMigrations, createSqliteD1 } from './helpers/d1-sqlite'
describe('skill detail SWR cache', () => {
const NOW_SEC = Math.floor(Date.now() / 1000)
const slug = 'ericzakariasson/scandinavian-design/alpha'
const cacheKey = `skills:detail:v3:${slug}`
const cacheKey = `skills:detail:v4:${slug}`
let harness: SqliteD1
let handler: (event: H3Event) => Promise<Record<string, unknown>>
let prepareCalls: number
Expand Down
6 changes: 3 additions & 3 deletions test/unit/skill-page-behaviors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ describe('skillPageBehaviors', () => {
})

expect(behaviors.map(behavior => [behavior.id, behavior.tier, behavior.locations])).toEqual([
['privilege', 'ask', [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md#L7' }]],
['shell', 'show', [{ path: 'SKILL.md', line: 6, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md#L6' }]],
['privilege', 'ask', [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md?plain=1#L7' }]],
['shell', 'show', [{ path: 'SKILL.md', line: 6, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md?plain=1#L6' }]],
['scripts', 'show', [{ path: 'scripts/install.sh', line: null, url: 'https://github.com/acme/skills/blob/main/skills/setup/scripts/install.sh' }]],
['packages', 'show', [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md#L7' }]],
['packages', 'show', [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/setup/SKILL.md?plain=1#L7' }]],
])
})

Expand Down