fix: merge Companion repository response validation

This commit is contained in:
2026-09-01 13:21:13 +09:00
3 changed files with 64 additions and 31 deletions
@@ -1,9 +1,20 @@
<script lang="ts"> <script lang="ts">
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { workspaceApiPath, workspaceRoute } from '$lib/workspace/api/http'; import { workspaceApiPath, workspaceRoute } from '$lib/workspace/api/http';
import type { RepositorySourceKind } from '$lib/generated/workspace-api';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
function sourceLabel(kind: RepositorySourceKind): string {
if (kind === 'local_path' || kind === 'file') return 'Local';
if (kind === 'invalid') return 'Invalid';
return 'Remote Git';
}
function supportsRepositoryAccess(kind: RepositorySourceKind): boolean {
return kind === 'ssh' || kind === 'http' || kind === 'https';
}
let showAddRepository = $state(false); let showAddRepository = $state(false);
let repositoryId = $state(''); let repositoryId = $state('');
let displayName = $state(''); let displayName = $state('');
@@ -122,22 +133,22 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data.repositories.items as repository (repository.repository_id)} {#each data.repositories.items as repository (repository.id)}
<tr> <tr>
<td> <td>
<a class="inline-link" href={workspaceRoute(data.workspaceId, `/repositories/${encodeURIComponent(repository.repository_id)}`)}> <a class="inline-link" href={workspaceRoute(data.workspaceId, `/repositories/${encodeURIComponent(repository.id)}`)}>
<strong>{repository.display_name}</strong> <strong>{repository.display_name}</strong>
</a> </a>
<small><code>{repository.repository_id}</code></small> <small><code>{repository.id}</code></small>
</td> </td>
<td> <td>
<span>{repository.source.kind === 'local_path' ? 'Local' : 'Remote Git'}</span> <span>{sourceLabel(repository.source.kind)}</span>
<small><code>{repository.source.uri}</code></small> <small><code>{repository.source.uri}</code></small>
</td> </td>
<td>{repository.default_selector ?? '—'}</td> <td>{repository.default_selector ?? '—'}</td>
<td>{repository.observed.status}</td> <td>{repository.observed_status}</td>
<td> <td>
{#if repository.source.kind === 'remote_git'} {#if supportsRepositoryAccess(repository.source.kind)}
<a class="inline-link" href={workspaceRoute(data.workspaceId, '/settings/repository-access')}>Configure access</a> <a class="inline-link" href={workspaceRoute(data.workspaceId, '/settings/repository-access')}>Configure access</a>
{:else} {:else}
<span class="settings-muted-action">Not required</span> <span class="settings-muted-action">Not required</span>
@@ -1,34 +1,13 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseRepositoryListResponse } from "$lib/workspace/api/workspace-model";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export type RepositorySource = {
kind: "local_path" | "remote_git";
uri: string;
};
export type RepositorySummary = {
repository_id: string;
display_name: string;
kind: string;
provider: string;
source: RepositorySource;
default_selector?: string | null;
observed: {
status: string;
observed_at?: string | null;
};
diagnostics: Array<{ code: string; message: string }>;
};
type RepositoryListResponse = {
items: RepositorySummary[];
diagnostics: Array<{ code: string; message: string }>;
};
export const load: PageLoad = async ({ fetch, params }) => { export const load: PageLoad = async ({ fetch, params }) => {
const repositories = await loadJson<RepositoryListResponse>( const repositories = await loadJson(
fetch, fetch,
workspaceApiPath(params.workspaceId, "/repositories"), workspaceApiPath(params.workspaceId, "/repositories"),
undefined,
parseRepositoryListResponse,
); );
return { return {
workspaceId: params.workspaceId, workspaceId: params.workspaceId,
@@ -1,5 +1,6 @@
declare const Deno: { declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void; test(name: string, fn: () => void | Promise<void>): void;
readTextFile(path: URL): Promise<string>;
}; };
import { import {
@@ -108,3 +109,45 @@ Deno.test("workspace response requires the permission projection", () => {
"permissions must be an object", "permissions must be an object",
); );
}); });
Deno.test("Repository settings consume the validated shared wire shape", async () => {
const [loadSource, pageSource] = await Promise.all([
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/repositories/+page.ts",
import.meta.url,
),
),
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/repositories/+page.svelte",
import.meta.url,
),
),
]);
for (
const token of [
"parseRepositoryListResponse",
"repository.id",
"repository.observed_status",
"sourceLabel(repository.source.kind)",
"supportsRepositoryAccess(repository.source.kind)",
]
) {
if (!loadSource.includes(token) && !pageSource.includes(token)) {
throw new Error(`Repository settings should include ${token}`);
}
}
for (
const staleToken of [
"repository.repository_id",
"repository.observed.status",
"repository.source.kind === 'remote_git'",
]
) {
if (loadSource.includes(staleToken) || pageSource.includes(staleToken)) {
throw new Error(`Repository settings must not use ${staleToken}`);
}
}
});