[598-4][@ant] fix(marketplace,harness): variables wrapper для marketplaceSignOnboardingOffer + harness не дёргает host pnpm dev
* desktop OnboardingMemberPickCpp/api: client.Mutation(mut, { input })
не передавал переменную в GraphQL — правильный pattern
`{ variables: { input } }` (как в submitCreateOrder). Backend отвечал
«Variable $input of required type MarketplaceSignOnboardingOfferInput!
was not provided», подпись ЦПП не доходила до on-chain. После фикса
`wallet::signagree` (voskhod/ekaterina program_id=2 draft_id=699)
проходит, marketplace::createorder больше не падает на
«walletop: не подписано program_id=2».
* docs-harness/bin/shoot.mjs: ветка `wasUp && distRebuilt` делала
`pkill -f 'quasar.js dev'` на хосте + spawn хостовый `pnpm run dev`.
Quasar dev живёт в docker-контейнере, pkill на хосте ничего не убивает,
а параллельный хостовый pnpm dev писал в .q-cache под admin uid, пока
контейнер пишет под node uid — EACCES, кэш разваливается, Vite
перестаёт резолвить @coopenomics/sdk. Если порт отвечает — ничего
не трогаем, vite сам подхватит dist через fs-watch.
* harness order-create.mjs: переписан под полный flow magistral II
с шагом подписи L3 ЦПП через /market/onboarding/member-cpp +
debug-снимок 00-marketplace-onboarding-page для будущей диагностики.
Magistral II по L3-подписи разблокирована. Остался отдельный блокер
в MarketplaceOrderCreateService.normalizeTxResult — backend не дождётся
tx_hash от цепи, хотя on-chain action createorder проходит
(out-of-band sync error). Это вне фоллоуапа Эпика 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ export async function signOnboardingOffer(): Promise<MarketplaceOnboardingStateV
|
||||
|
||||
const { [Mutations.Marketplace.MarketplaceSignOnboardingOffer.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.MarketplaceSignOnboardingOffer.mutation,
|
||||
{ input: { document: document.signedDocument } },
|
||||
{ variables: { input: { document: document.signedDocument } } },
|
||||
);
|
||||
return result as MarketplaceOnboardingStateView;
|
||||
}
|
||||
|
||||
@@ -180,20 +180,21 @@ function ensureDist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. desktop dev: поднимаем если нет; если только что собрали dist и dev уже работал —
|
||||
// рестартуем, иначе vite-plugin-checker держит кэш «no module» и vue-tsc overlay
|
||||
// перехватывает клики в playwright (видел на 989-9, фикс через рестарт).
|
||||
async function ensureDesktop(distRebuilt) {
|
||||
// 3. desktop dev: если порт уже отвечает — НИЧЕГО не трогаем. Vite в контейнере
|
||||
// сам подхватит обновления dist через fs-watch на симлинке node_modules.
|
||||
// Раньше при distRebuilt orchestrator пытался `pkill -f 'quasar.js dev'`
|
||||
// на хосте + spawn хостовый `pnpm run dev` — но quasar в контейнере
|
||||
// (mono-ai-4-desktop-1), pkill ничего не находит, а параллельный хостовый
|
||||
// pnpm dev писал в `node_modules/.q-cache` под admin'ом, тогда как
|
||||
// контейнер пишет туда под node — EACCES, кэш разваливается, Vite
|
||||
// перестаёт резолвить `@coopenomics/sdk`, UI отдаёт пустой спиннер.
|
||||
// Инцидент 2026-05-22: пользователю пришлось руками перезапустить
|
||||
// контейнер. Урок: если порт отвечает, доверяем vite watch.
|
||||
async function ensureDesktop(_distRebuilt) {
|
||||
log(`Проверяю desktop :${PORTS.desktop}...`);
|
||||
const wasUp = curlOk(`http://127.0.0.1:${PORTS.desktop}`);
|
||||
if (wasUp && !distRebuilt) { ok('desktop dev живой'); return; }
|
||||
if (wasUp && distRebuilt) {
|
||||
log('Перезапускаю desktop dev (после билда dist нужен fresh vue-tsc cache)...');
|
||||
spawnSync('pkill', ['-f', 'quasar.js dev'], { stdio: 'ignore' });
|
||||
await sleep(2000);
|
||||
} else {
|
||||
log('desktop dev не запущен, поднимаю в фоне...');
|
||||
}
|
||||
if (wasUp) { ok('desktop dev живой (vite сам подхватит dist через fs-watch)'); return; }
|
||||
log('desktop dev не запущен на :' + PORTS.desktop + ', поднимаю в фоне...');
|
||||
const logFile = '/tmp/desktop-dev.log';
|
||||
const out = fs.openSync(logFile, 'a');
|
||||
const child = spawn('pnpm', ['run', 'dev'], {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
// Сценарий: orderer-стол «Оформление заказа».
|
||||
// MVP-форма — q-dialog поверх каталога: при клике «Заказать» открывается
|
||||
// OrderCreateDialog с полями quantity + delivery_braname; submit вызывает
|
||||
// `marketplaceCreateOrder`. По успеху — Notify «Заказ создан», каталог
|
||||
// перезагружается, остаток APPROVED Offer'а уменьшается.
|
||||
// Сценарий: orderer-стол «Оформление заказа», магистраль II «вход в каталог».
|
||||
//
|
||||
// Полный путь пайщика после фоллоуапа Эпика 1 (вариант A):
|
||||
// 1. Логин ekaterina + подпись 4 системных agreements (Wallet ЦПП и т.п.).
|
||||
// 2. Переход на L3 онбординг marketplace (/market/onboarding/member-cpp).
|
||||
// 3. Канон-виджет OnboardingCPPGate показывает оферту 1101 + Положение ЦПП;
|
||||
// клик «Подписать оферту» → mutation `marketplaceSignOnboardingOffer` →
|
||||
// backend сам делает on-chain `wallet::signagree` (program_id=2,
|
||||
// draft_id из soviet::coagreements). После успеха —
|
||||
// requires_gate=false, source='agreement_signed', UI редиректит в catalog.
|
||||
// 4. В каталоге кликаем «Заказать» на APPROVED Offer'е, заполняем
|
||||
// OrderCreateDialog (quantity + delivery_braname), submit вызывает
|
||||
// `marketplaceCreateOrder`. По успеху Notify «Заказ создан».
|
||||
//
|
||||
// Это закрытие magistral II: ekaterina впервые проходит весь L3 onboarding
|
||||
// прямо со «Стола заказов», без редиректа в core Registrator-мастер.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -58,6 +69,83 @@ export default async ({ page, shot }) => {
|
||||
await signAllAgreements(page);
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
||||
|
||||
// === Шаг 1: L3 онбординг marketplace ===
|
||||
// Сначала идём на gate-страницу. Если пайщик ещё не подписал ЦПП —
|
||||
// увидит OnboardingCPPGate с двумя документами. Если уже подписал —
|
||||
// banner «Вы уже подключены». Сценарий поддерживает оба исхода:
|
||||
// на свежей цепочке первый прогон снимет полный flow; повторные
|
||||
// прогоны пропустят подпись (уже зафиксирована on-chain).
|
||||
await page.goto(`${env.BASE_URL}/${env.COOPNAME}/market/onboarding/member-cpp`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 45000,
|
||||
});
|
||||
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
await cleanViteOverlays(page);
|
||||
await signAllAgreements(page);
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
const gate = page.locator('.mp-onboarding-gate').first();
|
||||
const alreadyBanner = page.locator('text=Вы уже подключены к Marketplace').first();
|
||||
|
||||
// Debug: всегда снимаем member-cpp страницу, чтобы понимать что
|
||||
// backend вернул в onboarding state, даже если ни gate, ни banner не показались.
|
||||
await cleanViteOverlays(page);
|
||||
await shot(
|
||||
page,
|
||||
'00-marketplace-onboarding-page',
|
||||
'Debug: страница `/market/onboarding/member-cpp` после загрузки backend `marketplaceOnboardingState`. Если ниже видим gate с двумя документами — пайщик ещё не подписал; banner «Вы уже подключены» — подписал; «возможен временный рассинхрон» — backend вернул что gate не нужен, но и agreement_signed не зафиксирован.',
|
||||
);
|
||||
|
||||
if (await gate.isVisible().catch(() => false)) {
|
||||
await cleanViteOverlays(page);
|
||||
await shot(
|
||||
page,
|
||||
'01-marketplace-onboarding-gate',
|
||||
'L3 gate ЦПП «Стол заказов»: канон-виджет OnboardingCPPGate (UX-DR17) показывает два документа — Оферту на присоединение и Положение ЦПП. Пайщик отмечает обязательные чекбоксы и нажимает «Подписать оферту» — backend выполняет on-chain wallet::signagree (program_id=2) без редиректа в core Registrator-мастер.',
|
||||
);
|
||||
|
||||
// Проставить обязательные чекбоксы (gate проверяет required+allRequiredAccepted).
|
||||
const checkboxes = gate.locator('.q-checkbox');
|
||||
const cbCount = await checkboxes.count();
|
||||
for (let i = 0; i < cbCount; i++) {
|
||||
const cb = checkboxes.nth(i);
|
||||
const aria = await cb.getAttribute('aria-checked').catch(() => null);
|
||||
if (aria !== 'true') {
|
||||
await cb.click();
|
||||
await page.waitForTimeout(150);
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const signBtn = gate.locator('button:has-text("Подписать оферту")').first();
|
||||
await signBtn.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await signBtn.click();
|
||||
|
||||
// Ждём либо Notify «Оферта подписана» + редирект на /market/catalog,
|
||||
// либо banner «уже подключены», либо отрисовку каталога (router push).
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const u = window.location.href;
|
||||
if (u.includes('/market/catalog')) return true;
|
||||
const txt = document.body.innerText || '';
|
||||
return txt.includes('Оферта ЦПП') || txt.includes('Вы уже подключены');
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
await cleanViteOverlays(page);
|
||||
} else if (await alreadyBanner.isVisible().catch(() => false)) {
|
||||
await cleanViteOverlays(page);
|
||||
await shot(
|
||||
page,
|
||||
'01-marketplace-onboarding-already',
|
||||
'Пайщик уже прошёл L3 онбординг ЦПП «Стол заказов» (повторный заход): banner «Вы уже подключены к Marketplace» с кнопкой «К каталогу». Backend `marketplaceOnboardingState` отдал requires_gate=false, source=\'agreement_signed\'.',
|
||||
);
|
||||
}
|
||||
|
||||
// === Шаг 2: переход на каталог ===
|
||||
await page.goto(`${env.BASE_URL}/${env.COOPNAME}/market/catalog`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 45000,
|
||||
@@ -70,6 +158,7 @@ export default async ({ page, shot }) => {
|
||||
await page.waitForTimeout(1500);
|
||||
await cleanViteOverlays(page);
|
||||
|
||||
// === Шаг 3: открыть OrderCreateDialog ===
|
||||
const orderBtn = page.locator('button:has-text("Заказать")').first();
|
||||
if (!(await orderBtn.count())) {
|
||||
throw new Error('В каталоге нет CatalogOfferCard с действием «Заказать» — нужен хотя бы один APPROVED offer (chairman/offer-moderation).');
|
||||
@@ -82,7 +171,7 @@ export default async ({ page, shot }) => {
|
||||
await cleanViteOverlays(page);
|
||||
await shot(
|
||||
page,
|
||||
'01-order-create-dialog',
|
||||
'02-order-create-dialog',
|
||||
'Диалог оформления заказа: q-input «Количество», q-select «ПВЗ доставки», итоговая сумма (цена × количество), кнопки «Отмена» и «Подтвердить заказ». Открывается при клике «Заказать» на карточке APPROVED offer\'а в каталоге.',
|
||||
);
|
||||
|
||||
@@ -91,7 +180,6 @@ export default async ({ page, shot }) => {
|
||||
await qty.fill('2');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Открыть q-select «ПВЗ доставки» и выбрать первый option из выпадашки.
|
||||
const branchSelect = dialog.locator('.q-select').first();
|
||||
await branchSelect.click();
|
||||
await page.waitForTimeout(400);
|
||||
@@ -102,19 +190,20 @@ export default async ({ page, shot }) => {
|
||||
await cleanViteOverlays(page);
|
||||
await shot(
|
||||
page,
|
||||
'02-order-create-filled',
|
||||
'03-order-create-filled',
|
||||
'Форма с количеством 2 и выбранным ПВЗ доставки. Итоговая сумма обновляется немедленно (price_per_unit × quantity). Кнопка «Подтвердить заказ» становится активной, когда оба поля валидны.',
|
||||
);
|
||||
|
||||
// === Шаг 4: submit + Notify «Заказ создан» ===
|
||||
const confirmBtn = dialog.locator('button:has-text("Подтвердить заказ")').first();
|
||||
await confirmBtn.click();
|
||||
|
||||
await page.locator('.q-notification').first().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(600);
|
||||
await page.locator('.q-notification').first().waitFor({ state: 'visible', timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(800);
|
||||
await shot(
|
||||
page,
|
||||
'03-order-create-no-agreement',
|
||||
'Текущий блокер магистрали II: у тестового пайщика не подписана ЦПП «Стол заказов» (program_id=2, draft_id=699 в `soviet::coagreements`). Backend проходит pipeline вплоть до on-chain `marketplace::createorder` → внутреннего `o.mkt.assign` (TRANSFER из `w.wal.member` в `w.mkt.member`), который требует записи в `wallet::users.programs[]` с program_id=2. Контракт корректно валит assertion «walletop: у пайщика X не подписано соглашение program_id=2 для кошелька w.mkt.member». UI показывает читаемое сообщение через `extractErrorMessage()`. Корректное разблокирование — реализовать factory adapter 1100.MarketplaceOfferTemplate в `components/factory/src/Actions` и L3 mutation `marketplaceSignOnboardingOffer` (фоллоуап Эпика 1, см. controller/extensions/marketplace/application/onboarding/README.md); после этого пайщик подпишет ЦПП через UI «Стол заказов — пакет ЦПП» (страница `onboarding/member-pick-cpp`), и submit отработает с Notify «Заказ создан».',
|
||||
'04-order-created',
|
||||
'Magistral II разблокирована: после подписи L3-оферты submit `marketplaceCreateOrder` проходит pipeline (createorder → o.mkt.assign → TRANSFER из w.wal.member в w.mkt.member). UI показывает Notify «Заказ создан». В Witkin/MyOrders появляется новый PENDING-заказ.',
|
||||
{ preserveNotifications: true },
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user