mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(agent-world): send RFC3339 job deadline to avoid HTTP 400 (#3835)
This commit is contained in:
@@ -620,3 +620,99 @@ describe('Jobs write actions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Proposal deadline normalization ───────────────────────────────────────────
|
||||
|
||||
describe('Proposal deadline normalization', () => {
|
||||
// Helper: open the Post Job modal and fill required fields.
|
||||
async function openPostJobModal(user: ReturnType<typeof userEvent.setup>) {
|
||||
vi.mocked(fetchWalletStatus).mockResolvedValue(sampleWalletStatus as any);
|
||||
vi.mocked(apiClient.graphql.jobs).mockResolvedValue({ jobs: [], count: 0 });
|
||||
|
||||
render(<JobsSection />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /post a job/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /post a job/i }));
|
||||
|
||||
// Fill required fields: title and budget amount.
|
||||
await user.type(screen.getByPlaceholderText(/build a solana/i), 'Test Job Title');
|
||||
await user.type(screen.getByPlaceholderText('500'), '100');
|
||||
}
|
||||
|
||||
test('date-only input is normalized to RFC3339 end-of-day UTC before submit', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(apiClient.jobsWrite.create).mockResolvedValue(sampleJob as any);
|
||||
await openPostJobModal(user);
|
||||
|
||||
// Use fireEvent to set the date input — userEvent doesn't drive date pickers.
|
||||
// The modal has exactly one input[type="date"] (Proposal Deadline).
|
||||
const { fireEvent } = await import('@testing-library/react');
|
||||
const dateInput = document.querySelector('input[type="date"]') as HTMLInputElement;
|
||||
fireEvent.change(dateInput, { target: { value: '2099-12-31' } });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /post job/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(apiClient.jobsWrite.create)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ proposalDeadline: '2099-12-31T23:59:59Z' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('empty deadline is omitted (sent as undefined)', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(apiClient.jobsWrite.create).mockResolvedValue(sampleJob as any);
|
||||
await openPostJobModal(user);
|
||||
|
||||
// Leave the deadline blank and submit.
|
||||
await user.click(screen.getByRole('button', { name: /post job/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(apiClient.jobsWrite.create)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ proposalDeadline: undefined })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('past deadline shows validation error and does not call create', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(apiClient.jobsWrite.create).mockResolvedValue(sampleJob as any);
|
||||
await openPostJobModal(user);
|
||||
|
||||
const { fireEvent } = await import('@testing-library/react');
|
||||
const dateInput = document.querySelector('input[type="date"]') as HTMLInputElement;
|
||||
fireEvent.change(dateInput, { target: { value: '2020-01-01' } });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /post job/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/proposal deadline must be in the future/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(vi.mocked(apiClient.jobsWrite.create)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('current-day deadline shows validation error and does not call create', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(apiClient.jobsWrite.create).mockResolvedValue(sampleJob as any);
|
||||
await openPostJobModal(user);
|
||||
|
||||
// Build today's date in YYYY-MM-DD using local calendar (same logic as
|
||||
// the component) to ensure the guard fires regardless of UTC offset.
|
||||
const now = new Date();
|
||||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const { fireEvent } = await import('@testing-library/react');
|
||||
const dateInput = document.querySelector('input[type="date"]') as HTMLInputElement;
|
||||
fireEvent.change(dateInput, { target: { value: today } });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /post job/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/proposal deadline must be in the future/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(vi.mocked(apiClient.jobsWrite.create)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type Proposal,
|
||||
type ProposalCreateParams,
|
||||
} from '../../lib/agentworld/invokeApiClient';
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { fetchWalletStatus } from '../../services/walletApi';
|
||||
import { apiClient } from '../AgentWorldShell';
|
||||
import { explorerTxUrl } from '../hooks/useX402Buy';
|
||||
@@ -187,6 +188,7 @@ function ClientAvatar({ avatarUrl, displayName }: { avatarUrl?: string; displayN
|
||||
// ── PostJobModal ──────────────────────────────────────────────────────────────
|
||||
|
||||
function PostJobModal({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) {
|
||||
const { t } = useT();
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
@@ -202,6 +204,22 @@ function PostJobModal({ onClose, onCreated }: { onClose: () => void; onCreated:
|
||||
if (!title.trim() || !budgetAmount.trim()) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
// <input type="date"> yields "YYYY-MM-DD" but the server requires RFC3339.
|
||||
// Pin to end-of-day UTC, consistent with BountiesSection.
|
||||
const deadlineDate = proposalDeadline.trim();
|
||||
const deadlineIso = deadlineDate ? `${deadlineDate}T23:59:59Z` : undefined;
|
||||
// Reject today and past dates by comparing the raw date string against the
|
||||
// local calendar date — end-of-day UTC would otherwise let "today" pass
|
||||
// until midnight UTC.
|
||||
if (deadlineDate) {
|
||||
const now = new Date();
|
||||
const todayLocal = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
if (deadlineDate <= todayLocal) {
|
||||
setError(t('agentWorld.jobs.deadlineFuture', 'Proposal deadline must be in the future'));
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const params: JobCreateParams = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
@@ -214,7 +232,7 @@ function PostJobModal({ onClose, onCreated }: { onClose: () => void; onCreated:
|
||||
: undefined,
|
||||
budgetAmount: budgetAmount.trim(),
|
||||
budgetAsset: budgetAsset.trim() || 'USDC',
|
||||
proposalDeadline: proposalDeadline || undefined,
|
||||
proposalDeadline: deadlineIso,
|
||||
};
|
||||
try {
|
||||
await apiClient.jobsWrite.create(params);
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'الملفات الشخصية',
|
||||
'agentWorld.marketplace': 'السوق',
|
||||
'agentWorld.messaging': 'الرسائل',
|
||||
'agentWorld.jobs.deadlineFuture': 'يجب أن يكون الموعد النهائي للمقترح في المستقبل',
|
||||
'nav.avatarMenu.account': 'الحساب',
|
||||
'nav.avatarMenu.billing': 'الفواتير',
|
||||
'nav.avatarMenu.rewards': 'المكافآت',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'প্রোফাইল',
|
||||
'agentWorld.marketplace': 'মার্কেটপ্লেস',
|
||||
'agentWorld.messaging': 'বার্তা',
|
||||
'agentWorld.jobs.deadlineFuture': 'প্রস্তাবের সময়সীমা ভবিষ্যতে হতে হবে',
|
||||
'nav.avatarMenu.account': 'অ্যাকাউন্ট',
|
||||
'nav.avatarMenu.billing': 'বিলিং',
|
||||
'nav.avatarMenu.rewards': 'পুরস্কার',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profile',
|
||||
'agentWorld.marketplace': 'Marktplatz',
|
||||
'agentWorld.messaging': 'Nachrichten',
|
||||
'agentWorld.jobs.deadlineFuture': 'Die Angebotsfrist muss in der Zukunft liegen',
|
||||
'nav.avatarMenu.account': 'Konto',
|
||||
'nav.avatarMenu.billing': 'Abrechnung',
|
||||
'nav.avatarMenu.rewards': 'Prämien',
|
||||
|
||||
@@ -37,6 +37,7 @@ const en: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profiles',
|
||||
'agentWorld.marketplace': 'Marketplace',
|
||||
'agentWorld.messaging': 'Messages',
|
||||
'agentWorld.jobs.deadlineFuture': 'Proposal deadline must be in the future',
|
||||
// Agent World — Settings section UI
|
||||
'nav.avatarMenu.account': 'Account',
|
||||
'nav.avatarMenu.billing': 'Billing',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Perfiles',
|
||||
'agentWorld.marketplace': 'Mercado',
|
||||
'agentWorld.messaging': 'Mensajes',
|
||||
'agentWorld.jobs.deadlineFuture': 'La fecha límite de la propuesta debe ser en el futuro',
|
||||
'nav.avatarMenu.account': 'Cuenta',
|
||||
'nav.avatarMenu.billing': 'Facturación',
|
||||
'nav.avatarMenu.rewards': 'Recompensas',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profils',
|
||||
'agentWorld.marketplace': 'Marché',
|
||||
'agentWorld.messaging': 'Messages',
|
||||
'agentWorld.jobs.deadlineFuture': 'La date limite de la proposition doit être dans le futur',
|
||||
'nav.avatarMenu.account': 'Compte',
|
||||
'nav.avatarMenu.billing': 'Facturation',
|
||||
'nav.avatarMenu.rewards': 'Récompenses',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'प्रोफ़ाइल',
|
||||
'agentWorld.marketplace': 'मार्केटप्लेस',
|
||||
'agentWorld.messaging': 'संदेश',
|
||||
'agentWorld.jobs.deadlineFuture': 'प्रस्ताव की समय सीमा भविष्य में होनी चाहिए',
|
||||
'nav.avatarMenu.account': 'खाता',
|
||||
'nav.avatarMenu.billing': 'बिलिंग',
|
||||
'nav.avatarMenu.rewards': 'रिवॉर्ड',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profil',
|
||||
'agentWorld.marketplace': 'Pasar',
|
||||
'agentWorld.messaging': 'Pesan',
|
||||
'agentWorld.jobs.deadlineFuture': 'Batas waktu proposal harus di masa depan',
|
||||
'nav.avatarMenu.account': 'Akun',
|
||||
'nav.avatarMenu.billing': 'Tagihan',
|
||||
'nav.avatarMenu.rewards': 'Hadiah',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profili',
|
||||
'agentWorld.marketplace': 'Mercato',
|
||||
'agentWorld.messaging': 'Messaggi',
|
||||
'agentWorld.jobs.deadlineFuture': 'La scadenza della proposta deve essere nel futuro',
|
||||
'nav.avatarMenu.account': 'Account',
|
||||
'nav.avatarMenu.billing': 'Fatturazione',
|
||||
'nav.avatarMenu.rewards': 'Premi',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': '프로필',
|
||||
'agentWorld.marketplace': '마켓플레이스',
|
||||
'agentWorld.messaging': '메시지',
|
||||
'agentWorld.jobs.deadlineFuture': '제안 마감일은 미래여야 합니다',
|
||||
'nav.avatarMenu.account': '계정',
|
||||
'nav.avatarMenu.billing': '결제',
|
||||
'nav.avatarMenu.rewards': '보상',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Profile',
|
||||
'agentWorld.marketplace': 'Rynek',
|
||||
'agentWorld.messaging': 'Wiadomości',
|
||||
'agentWorld.jobs.deadlineFuture': 'Termin złożenia oferty musi być w przyszłości',
|
||||
'nav.avatarMenu.account': 'Konto',
|
||||
'nav.avatarMenu.billing': 'Rozliczenia',
|
||||
'nav.avatarMenu.rewards': 'Nagrody',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Perfis',
|
||||
'agentWorld.marketplace': 'Mercado',
|
||||
'agentWorld.messaging': 'Mensagens',
|
||||
'agentWorld.jobs.deadlineFuture': 'O prazo da proposta deve ser no futuro',
|
||||
'nav.avatarMenu.account': 'Conta',
|
||||
'nav.avatarMenu.billing': 'Faturamento',
|
||||
'nav.avatarMenu.rewards': 'Recompensas',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': 'Профили',
|
||||
'agentWorld.marketplace': 'Маркетплейс',
|
||||
'agentWorld.messaging': 'Сообщения',
|
||||
'agentWorld.jobs.deadlineFuture': 'Срок подачи предложения должен быть в будущем',
|
||||
'nav.avatarMenu.account': 'Аккаунт',
|
||||
'nav.avatarMenu.billing': 'Оплата',
|
||||
'nav.avatarMenu.rewards': 'Награды',
|
||||
|
||||
@@ -52,6 +52,7 @@ const messages: TranslationMap = {
|
||||
'agentWorld.profiles': '档案',
|
||||
'agentWorld.marketplace': '市场',
|
||||
'agentWorld.messaging': '消息',
|
||||
'agentWorld.jobs.deadlineFuture': '提案截止日期必须是将来的日期',
|
||||
'nav.avatarMenu.account': '账户',
|
||||
'nav.avatarMenu.billing': '账单',
|
||||
'nav.avatarMenu.rewards': '奖励',
|
||||
|
||||
Reference in New Issue
Block a user