diff --git a/layers/identity/server/utils/scan-owned-repos.ts b/layers/identity/server/utils/scan-owned-repos.ts index 6a2d3fa6..50f789f5 100644 --- a/layers/identity/server/utils/scan-owned-repos.ts +++ b/layers/identity/server/utils/scan-owned-repos.ts @@ -278,7 +278,7 @@ async function requestOrgList( url: string, userToken: string, ): Promise { - const outcome = await fetchImpl(url, { + const outcome = await fetchImpl.call(globalThis, url, { headers: { 'Authorization': `Bearer ${userToken}`, 'Accept': 'application/vnd.github+json', @@ -324,7 +324,9 @@ export function makeOwnedRepoScanner(deps: ScanOwnedReposDependencies) { for (const owner of owners) { for (let page = 1; page <= MAX_PAGES; page++) { const q = encodeURIComponent(`filename:SKILL.md ${owner.qualifier} is:public`) - const responseOutcome = await deps.fetch( + // Worker fetch requires the global receiver, not the dependency object. + const responseOutcome = await deps.fetch.call( + globalThis, `https://api.github.com/search/code?q=${q}&per_page=${PER_PAGE}&page=${page}`, { headers: { diff --git a/nuxt.config.ts b/nuxt.config.ts index f065f929..277f4862 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -40,6 +40,8 @@ const hasSentryAuthToken = Boolean(process.env.SENTRY_AUTH_TOKEN) export default defineNuxtConfig({ checkin: { external: externalCheckin }, + // Private components need their parent's props and must never become pages. + ignore: ['**/pages/**/_*.vue'], extends: ['./layers/admin', './layers/artifact-delivery', './layers/identity', './layers/registry', './layers/marketing', './layers/mcp'], hooks: { diff --git a/test/unit/private-page-components.nuxt.test.ts b/test/unit/private-page-components.nuxt.test.ts new file mode 100644 index 00000000..f0f81f96 --- /dev/null +++ b/test/unit/private-page-components.nuxt.test.ts @@ -0,0 +1,10 @@ +import { useRouter } from '#app' + +describe('private page components', () => { + it.each(['/_HomeDemoCarousel', '/brand-kit/_GithubBadgePreview'])( + 'does not resolve %s as a page', + (path) => { + expect(useRouter().resolve(path).name).toBeUndefined() + }, + ) +}) diff --git a/test/unit/scan-owned-repos.test.ts b/test/unit/scan-owned-repos.test.ts index e9efb0c9..0c17f894 100644 --- a/test/unit/scan-owned-repos.test.ts +++ b/test/unit/scan-owned-repos.test.ts @@ -3,6 +3,7 @@ import Database from 'better-sqlite3' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { buildScanOwners, + makeGithubOrgLister, makeOwnedRepoScanner, ownedRepoScanResponse, ownedRepoScanWarning, @@ -62,6 +63,38 @@ describe('owned GitHub repository discovery', () => { afterEach(() => sqlite.close()) + it('calls the Worker fetch with its global receiver', async () => { + const fetchImpl = function (this: unknown): Promise { + if (this !== globalThis) + throw new TypeError('Illegal invocation') + return Promise.resolve(Response.json(searchBody({ total_count: 0, items: [] }))) + } + const scan = makeOwnedRepoScanner({ + fetch: fetchImpl, + listOrgs: noOrgs, + syncRepo: vi.fn(), + resolveGithubBindings: () => ({}), + now: () => 100, + claimToken: () => 'claim-one', + }) + + await expect(scan({ login: 'acme', userToken: 'token', db, env: {} as Cloudflare.Env })) + .resolves + .toMatchObject({ _tag: 'complete', reposFound: 0 }) + }) + + it('preserves the Worker fetch receiver when listing organisations', async () => { + const listOrgs = makeGithubOrgLister(function (this: unknown): Promise { + if (this !== globalThis) + throw new TypeError('Illegal invocation') + return Promise.resolve(Response.json([{ login: 'acme-org' }])) + }) + + await expect(listOrgs({ login: 'acme', userToken: 'token' })) + .resolves + .toEqual({ _tag: 'orgs', logins: ['acme-org'], visibility: 'member' }) + }) + it('persists an unknown candidate before a failed sync without creating a skill row', async () => { const syncRepo = vi.fn(async (owner: string, repo: string) => ({ ...indexed(owner, repo),