Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/dashboard/app/orders/new/compose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest';
import { composeOrderText } from './compose';

describe('composeOrderText', () => {
it('composes a full natural-language order from structured fields', () => {
const text = composeOrderText({
workerDisplayName: 'Mameta29',
repository: 'Mameta29/agentic-gig-flow',
amountJpyc: '50000',
deadline: '1週間後',
description: 'Dashboard 発注フォームの改善',
acceptanceCriteria: '既存テストがすべて通る、CIが通過している',
});
// Worker, repository, amount, deadline, description and criteria must all
// survive into the text Contract Agent parses.
expect(text).toContain('Mameta29 さんに');
expect(text).toContain('Dashboard 発注フォームの改善 を');
expect(text).toContain('50,000 JPYC で');
expect(text).toContain('1週間後 まで');
expect(text).toContain('リポジトリは Mameta29/agentic-gig-flow。');
expect(text).toContain(
'受け入れ基準: 既存テストがすべて通る、CIが通過している。',
);
});

it('formats the amount with thousands separators', () => {
const text = composeOrderText({
workerDisplayName: 'Sato Taro',
repository: 'Mameta29/gigflow-demo-workspace',
amountJpyc: '80000',
deadline: '3週間後',
description: 'リファクタ',
});
expect(text).toContain('80,000 JPYC');
});

it('omits optional acceptance criteria when empty', () => {
const text = composeOrderText({
workerDisplayName: 'Sato Taro',
repository: 'Mameta29/gigflow-demo-workspace',
amountJpyc: '30000',
deadline: '1週間後',
description: 'バグ修正',
});
expect(text).not.toContain('受け入れ基準');
});

it('drops a non-numeric amount instead of emitting NaN', () => {
const text = composeOrderText({
workerDisplayName: 'Sato Taro',
repository: 'Mameta29/gigflow-demo-workspace',
amountJpyc: 'abc',
deadline: '1週間後',
description: 'バグ修正',
});
expect(text).not.toContain('NaN');
expect(text).not.toContain('JPYC で');
});
});
38 changes: 38 additions & 0 deletions packages/dashboard/app/orders/new/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Pure helper extracted from page.tsx so it can be unit-tested without
// rendering the client component (Next.js page files may only export the
// default component + reserved names).

/**
* Compose a natural-language order sentence from the structured selections.
* Contract Agent still parses this text — the natural-language entry point is
* intentionally preserved. The form just removes the typo class of errors
* ("受注者が登録されていません" / "リポジトリが登録されていません") by letting the
* PM pick the worker and repository from the tenant's allow-list instead of
* free-typing them.
*/
export function composeOrderText(input: {
workerDisplayName: string;
repository: string;
amountJpyc: string;
deadline: string;
description: string;
acceptanceCriteria?: string;
}): string {
const amount = input.amountJpyc.trim();
const amountLabel =
amount && Number.isFinite(Number(amount))
? `${Number(amount).toLocaleString('en-US')} JPYC`
: '';
const parts = [
`${input.workerDisplayName} さんに`,
input.description.trim() ? `${input.description.trim()} を` : '',
amountLabel ? `${amountLabel} で` : '',
input.deadline.trim() ? `${input.deadline.trim()} まで` : '',
'お願いします。',
`リポジトリは ${input.repository}。`,
input.acceptanceCriteria?.trim()
? `受け入れ基準: ${input.acceptanceCriteria.trim()}。`
: '',
].filter(Boolean);
return parts.join(' ');
}
Loading
Loading