fix batch 1

This commit is contained in:
Pavel committed 2026-09-29 12:19:06 +04:00
1 parent 7530e54aec
commit 49a3a2a220
6 files changed
+87 -7

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ class AgentsRepository:
"limited_token_mode", "limited_request_mode",
"allow_system_prompt_override",
"shared", "shared_token", "shared_metadata",
"tools", "json_schema", "models", "config", "legacy_mongo_id",
"tools", "json_schema", "models", "config", "resource_sponsors", "legacy_mongo_id",
"created_at", "updated_at", "last_used_at",
}
@@ -1,6 +1,9 @@
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import type { Agent, ResourceSponsor } from '../types';
import SponsoredResourcesNotice from './SponsoredResourcesNotice';
@@ -86,6 +89,36 @@ describe('SponsoredResourcesNotice', () => {
expect(container.innerHTML).toBe('');
});
it('joins names for the app language and does not escape them', async () => {
await i18n.use(initReactI18next).init({
lng: 'jp',
resources: {
jp: {
translation: {
agents: {
form: { sponsors: { addedBy: '{{person}}: {{names}}' } },
},
},
},
},
});
try {
await render({
...baseAgent,
resource_sponsors: [
sponsor({ id: 'docs/a' }),
sponsor({ id: 'docs/b' }),
],
});
const expected = new Intl.ListFormat('ja', {
type: 'conjunction',
}).format(['name-docs/a', 'name-docs/b']);
expect(container.textContent).toBe(`bob@example.com: ${expected}`);
} finally {
await i18n.changeLanguage('en');
}
});
it('splits running and no-longer-running items', async () => {
await render({
...baseAgent,
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { can, isOwner } from '../../utils/accessUtils';
import { intlLocale } from '../../utils/dateTimeUtils';
import type { Agent, ResourceSponsor } from '../types';
type SponsoredResourcesNoticeProps = {
@@ -34,7 +35,7 @@ export default function SponsoredResourcesNotice({
agent,
resolveName,
}: SponsoredResourcesNoticeProps) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const sponsors = agent.resource_sponsors ?? [];
const showAttachNote = !isOwner(agent) && can(agent, 'edit');
const active = groupByPerson(sponsors.filter((s) => s.active));
@@ -42,8 +43,11 @@ export default function SponsoredResourcesNotice({
if (!showAttachNote && sponsors.length === 0) return null;
const listFormat = new Intl.ListFormat(intlLocale(i18n.language), {
type: 'conjunction',
});
const names = (items: ResourceSponsor[]) =>
items.map((item) => resolveName(item)).join(', ');
listFormat.format(items.map((item) => resolveName(item)));
return (
<div className="flex flex-col gap-3 sm:col-span-2">
@@ -63,6 +67,7 @@ export default function SponsoredResourcesNotice({
{active.map(([person, items]) => (
<p key={person}>
{t('agents.form.sponsors.addedBy', {
interpolation: { escapeValue: false },
person,
names: names(items),
})}
@@ -78,6 +83,7 @@ export default function SponsoredResourcesNotice({
{inactive.map(([person, items]) => (
<p key={person}>
{t('agents.form.sponsors.unavailable', {
interpolation: { escapeValue: false },
person,
names: names(items),
})}
+11 -4
View File
@@ -157,8 +157,15 @@ export function capabilityLines(
}
// A teams API error carries the server's message; anything else (a network
// failure) falls back to the caller's generic copy.
export const errorMessage = (error: unknown, fallback: string): string =>
error instanceof Error && error.name === 'TeamsApiError' && error.message
? error.message
// failure) falls back to the caller's generic copy. Matched by shape, not
// `instanceof`: a thunk's unwrap() rethrows a serialized plain object.
export const errorMessage = (error: unknown, fallback: string): string => {
const e = error as { name?: unknown; message?: unknown } | null;
return typeof e === 'object' &&
e !== null &&
e.name === 'TeamsApiError' &&
typeof e.message === 'string' &&
e.message
? e.message
: fallback;
};
+28
View File
@@ -10,6 +10,7 @@ vi.mock('../api/services/teamsService', () => ({
import { configureStore } from '@reduxjs/toolkit';
import { errorMessage } from './accessSettings';
import reducer, { deleteTeam, loadTeams, Team } from './teamsSlice';
const team: Team = { id: 't1', name: 'Ops', slug: 'ops', owner_id: 'u1' };
@@ -41,6 +42,33 @@ describe('teamsSlice', () => {
expect(store.getState().teams.teams).toHaveLength(1);
});
it("surfaces the server's reason through unwrap()", async () => {
// unwrap() rethrows a serialized plain object, not the Error instance.
remove.mockRejectedValue(
Object.assign(new Error('Only the owner can delete'), {
name: 'TeamsApiError',
}),
);
const store = makeStore();
const rejection = await store
.dispatch(deleteTeam({ id: 't1', token: null }))
.unwrap()
.catch((error: unknown) => error);
expect(errorMessage(rejection, 'fallback')).toBe(
'Only the owner can delete',
);
});
it('keeps the fallback for a non-teams rejection', async () => {
remove.mockRejectedValue(new Error('network down'));
const store = makeStore();
const rejection = await store
.dispatch(deleteTeam({ id: 't1', token: null }))
.unwrap()
.catch((error: unknown) => error);
expect(errorMessage(rejection, 'fallback')).toBe('fallback');
});
it('keeps the team when a 2xx body says success:false', async () => {
remove.mockResolvedValue({ success: false, message: 'nope' });
const store = makeStore();
@@ -49,6 +49,12 @@ class TestCreate:
)
assert doc["legacy_mongo_id"] == "507f1f77bcf86cd799439011"
def test_create_with_resource_sponsors(self, pg_conn):
repo = _repo(pg_conn)
sponsors = {"tool:t1": "editor-1"}
doc = repo.create("u", "a", "draft", resource_sponsors=sponsors)
assert doc["resource_sponsors"] == sponsors
def test_create_normalizes_blank_key_to_null(self, pg_conn):
repo = _repo(pg_conn)
doc = repo.create("u", "a", "draft", key="")