fix(ROUTING-0001): repair repo routing and isolate MCP filtering
This commit is contained in:
@@ -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
|
||||
|
||||
27
src/lib/components/RepositoryCard.svelte.test.ts
Normal file
27
src/lib/components/RepositoryCard.svelte.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
34
src/routes/repos/[id]/page.server.test.ts
Normal file
34
src/routes/repos/[id]/page.server.test.ts
Normal 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' }]
|
||||
});
|
||||
});
|
||||
});
|
||||
33
src/routes/route-file-conventions.test.ts
Normal file
33
src/routes/route-file-conventions.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user