fix(ROUTING-0001): repair repo routing and isolate MCP filtering

This commit is contained in:
Giancarmine Salucci
2026-03-27 19:01:47 +01:00
parent da661efc91
commit d1381f7fc0
10 changed files with 252 additions and 644 deletions

View File

@@ -26,6 +26,10 @@
error: 'Error'
};
const detailsHref = $derived(
resolveRoute('/repos/[id]', { id: encodeURIComponent(repo.id) })
);
const totalSnippets = $derived(repo.totalSnippets ?? 0);
const trustScore = $derived(repo.trustScore ?? 0);
</script>
@@ -77,7 +81,7 @@
{repo.state === 'indexing' ? 'Indexing...' : 'Re-index'}
</button>
<a
href={resolveRoute('/repos/[id]', { id: repo.id })}
href={detailsHref}
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
>
Details

View File

@@ -0,0 +1,27 @@
import { page } from 'vitest/browser';
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import RepositoryCard from './RepositoryCard.svelte';
describe('RepositoryCard.svelte', () => {
it('encodes slash-bearing repository ids in the details href', async () => {
render(RepositoryCard, {
repo: {
id: '/facebook/react',
title: 'React',
description: 'A JavaScript library for building user interfaces',
state: 'indexed',
totalSnippets: 1234,
trustScore: 9.7,
stars: 230000,
lastIndexedAt: null
} as never,
onReindex: vi.fn(),
onDelete: vi.fn()
});
await expect
.element(page.getByRole('link', { name: 'Details' }))
.toHaveAttribute('href', '/repos/%2Ffacebook%2Freact');
});
});

View File

@@ -39,6 +39,7 @@ import { GET as getJobs } from './jobs/+server.js';
import { GET as getJob } from './jobs/[id]/+server.js';
import { GET as getVersions, POST as postVersions } from './libs/[id]/versions/+server.js';
import { GET as getContext } from './context/+server.js';
import { DEFAULT_TOKEN_BUDGET } from '$lib/server/api/token-budget.js';
const NOW_S = Math.floor(Date.now() / 1000);
@@ -325,6 +326,40 @@ describe('API contract integration', () => {
expect(body).toContain('Result count: 0');
});
it('GET /api/v1/context does not token-filter default JSON responses for the UI', async () => {
const repositoryId = seedRepo(db);
const documentId = seedDocument(db, repositoryId);
seedSnippet(db, {
documentId,
repositoryId,
type: 'info',
title: 'Large result',
content: 'Large result body',
tokenCount: DEFAULT_TOKEN_BUDGET + 1
});
seedSnippet(db, {
documentId,
repositoryId,
type: 'info',
title: 'Small result',
content: 'Small result body',
tokenCount: 5
});
const response = await getContext({
url: new URL(
`http://test/api/v1/context?libraryId=${encodeURIComponent(repositoryId)}&query=${encodeURIComponent('result')}`
)
} as never);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.snippets).toHaveLength(2);
expect(body.resultCount).toBe(2);
});
it('GET /api/v1/context returns additive repository and version metadata for versioned results', async () => {
const repositoryId = seedRepo(db);
const versionId = seedVersion(db, repositoryId, 'v18.3.0');

View File

@@ -124,6 +124,7 @@ export const GET: RequestHandler = async ({ url }) => {
}
const responseType = url.searchParams.get('type') ?? 'json';
const applyTokenBudget = responseType === 'txt' || url.searchParams.has('tokens');
const tokensRaw = parseInt(url.searchParams.get('tokens') ?? String(DEFAULT_TOKEN_BUDGET), 10);
const maxTokens = isNaN(tokensRaw) || tokensRaw < 1 ? DEFAULT_TOKEN_BUDGET : tokensRaw;
@@ -212,15 +213,17 @@ export const GET: RequestHandler = async ({ url }) => {
profileId
});
// Apply token budget.
const snippets = searchResults.map((r) => r.snippet);
const selected = selectSnippetsWithinBudget(snippets, maxTokens);
const selectedResults = applyTokenBudget
? (() => {
const snippets = searchResults.map((r) => r.snippet);
const selected = selectSnippetsWithinBudget(snippets, maxTokens);
// Re-wrap selected snippets as SnippetSearchResult for formatters.
const selectedResults = selected.map((snippet) => {
const found = searchResults.find((r) => r.snippet.id === snippet.id)!;
return found;
});
return selected.map((snippet) => {
const found = searchResults.find((r) => r.snippet.id === snippet.id)!;
return found;
});
})()
: searchResults;
const snippetVersionIds = Array.from(
new Set(

View File

@@ -2,8 +2,8 @@ import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ fetch, params }) => {
const id = params.id;
const res = await fetch(`/api/v1/libs/${encodeURIComponent(id)}`);
const repositoryId = decodeURIComponent(params.id);
const res = await fetch(`/api/v1/libs/${encodeURIComponent(repositoryId)}`);
if (res.status === 404) {
error(404, 'Repository not found');
@@ -16,7 +16,9 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
const repo = await res.json();
// Fetch recent jobs
const jobsRes = await fetch(`/api/v1/jobs?repositoryId=${encodeURIComponent(id)}&limit=5`);
const jobsRes = await fetch(
`/api/v1/jobs?repositoryId=${encodeURIComponent(repositoryId)}&limit=5`
);
const jobsData = jobsRes.ok ? await jobsRes.json() : { jobs: [] };
return {

View File

@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from 'vitest';
import { load } from './+page.server';
describe('/repos/[id] page server load', () => {
it('decodes the route param once before calling downstream APIs', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: '/facebook/react', title: 'React' })
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ jobs: [{ id: 'job-1', repositoryId: '/facebook/react' }] })
});
const result = await load({
fetch,
params: { id: encodeURIComponent('/facebook/react') }
} as never);
expect(fetch).toHaveBeenNthCalledWith(1, '/api/v1/libs/%2Ffacebook%2Freact');
expect(fetch).toHaveBeenNthCalledWith(
2,
'/api/v1/jobs?repositoryId=%2Ffacebook%2Freact&limit=5'
);
expect(result).toEqual({
repo: { id: '/facebook/react', title: 'React' },
recentJobs: [{ id: 'job-1', repositoryId: '/facebook/react' }]
});
});
});

View File

@@ -0,0 +1,33 @@
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
function collectReservedRouteTestFiles(directory: string): string[] {
const entries = readdirSync(directory, { withFileTypes: true });
const reservedTestFiles: string[] = [];
for (const entry of entries) {
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
reservedTestFiles.push(...collectReservedRouteTestFiles(entryPath));
continue;
}
if (!entry.name.startsWith('+')) continue;
if (!entry.name.includes('.test.') && !entry.name.includes('.spec.')) continue;
reservedTestFiles.push(entryPath);
}
return reservedTestFiles;
}
describe('SvelteKit route file conventions', () => {
it('does not place test files in reserved +prefixed route filenames', () => {
const routeDirectory = import.meta.dirname;
const reservedTestFiles = collectReservedRouteTestFiles(routeDirectory);
expect(reservedTestFiles).toEqual([]);
});
});