این تحویل، قرارداد ایجاد factory سناریوهای QA ایزوله را روی lifecycle امن و disposable موجود کامل میکند. هدف آن ساختن دقیقاً داده لازم هر سناریو است؛ نه کپیکردن baseline داده نمایشی و نه پاکسازی یک پایگاه مشترک.
interface عمومی withQaScenario در scripts/qa/scenario.v1.mjs است و شکل ماشینخوان
قرارداد در ops/qa/scenario-contract.v1.json نگهداری میشود. هر فراخوانی:
- از نام سناریو و entropy محلی،
runIdیکتای حداکثر ۳۱ نویسه و namespace با پیشوندsevo.qa.میسازد؛ - PostgreSQL و MinIO تازه را با profile صریح
qaو migration ledger مشترک بالا میآورد؛ - context نسخهدار شامل ساعت UTC ثابت، شناسهساز UUID v5 وابسته به namespace، endpointها و
scenario.environmentکامل همان اجرای disposable را بهbuildمیدهد؛ - فقط دادهای را میسازد که callback همان سناریو درخواست کرده است؛
exerciseرا اجرا و در موفقیت یا شکست، teardown را فقط با جفتrunId + fingerprintهمان محیط انجام میدهد.
نمونه استفاده در تست:
await withQaScenario(
{
name: "payment-refused",
fixedTime: "2026-08-30T08:30:00.000Z",
async build(scenario) {
const orderId = scenario.id("order");
// فقط commandهای لازم همین سفارش را با scenario.clock.now() اجرا کنید.
return { orderId };
},
},
async (scenario) => {
// رفتار عمومی را با scenario.data.orderId بیازمایید.
},
);نامهای ورودی lowercase و پایدارند، fixedTime باید timestamp کامل UTC باشد و نامهای داده
برای scenario.id() نیز کلید پایدار lowercase هستند. مقدار برگشتی build تنها داده قابل
مشاهده exercise است؛ بنابراین داده جانبی یا setup عمومی پنهان وارد سناریو نمیشود.
- adapter هیچ
DATABASE_URLورودی نمیپذیرد، مقدار ارثی shell/CI را حذف میکند و مقصد را فقط از پورت گزارششده lifecycle همان اجرا میسازد. - runner مستقل
pnpm test:qa-scenarioپیش از ساخت process آزمون،DATABASE_URLرا حذف وSEVO_RUNTIME_ENV=testو OTP داخلیdevرا قطعی میکند. خودwithQaScenarioهمین env را پیش از callbackهایbuildوexerciseدوباره بررسی میکند. هر callback برای composition واقعی برنامه بایدscenario.environmentرا مصرف کند؛ این مقدار database و MinIO همان اجرای disposable، credential محلی ثابت، OTP داخلی و telemetry خاموش را حمل میکند و هیچ مقصد انسانی یا provider بیرونی process والد را عبور نمیدهد. - lifecycle پیش از write، profile و نام allowlistشده پایگاه را از خود مقصد و fingerprint آن را بررسی میکند. گزارش ناهماهنگ اجرا را متوقف میکند و با proof معتبر، teardown owner-scoped را پیش از بازگرداندن خطا تلاش میکند.
- گزارش startup برای factory روی file descriptor اختصاصی نوشته میشود و stdout فقط خروجی انسانی است. اگر نوشتن کانال اجباری شکست بخورد، initialization ناموفق میشود و coordinator موجود پیش از خروج، پروژه و claim همان run را حذف میکند؛ خروجی ناقص نمیتواند محیط orphan و ظاهراً موفق باقی بگذارد.
- اگر هم رفتار سناریو و هم teardown شکست بخورند، هر دو خطا در
AggregateErrorحفظ میشوند؛ موفق نشاندادن سناریویی که محیطش پاک نشده مجاز نیست. - factory هیچ import یا receipt از
demo:seedو manifest داده نمایشی ندارد. حذف محیط QA نیز داده یا volume متعلق به local، CI job دیگر یا اجرای QA دیگر را هدف نمیگیرد.
local و CI، runner مستقل را روی host اجرا میکنند و runner برای هر تست stack تازه را از
compose.yaml بالا میآورد؛ اجرای factory داخل container برنامه یا اتصال به stack از پیش
موجود پشتیبانی نمیشود. lifecycle همان فرمان prisma migrate deploy رسمی را در container
PostgreSQL disposable اجرا میکند، اما startup، env یا پورت مسیرهای runtime برنامه در
pnpm dev و docker compose up --build را تغییر نمیدهد. تست integration واقعی یک هویت
کمینه با timestamp ثابت میسازد، خالیبودن receiptهای demo را میسنجد و پس از پایان نبود
container، network، volume و ownership claim همان run را بررسی میکند. regression دوم نیز
شکست کانال report را ایجاد و cleanup کامل startup ناموفق را ثابت میکند.
این تغییر endpoint HTTP، payload برنامه، schema پایگاه یا رفتار runtime خریدار/فروشنده را
عوض نمیکند؛ در نتیجه OpenAPI و migration تازهای ندارد. قرارداد افزودهشده فقط
QA scenario v1 است و تغییر ناسازگار آینده باید در فایل و entrypoint نسخه تازه منتشر شود.