feat(web-test): управление пулом контекстов/лицензий в раннере

Раннер регресса теперь держит пул 1С-сеансов сам, вместо накопления контекстов
между тестами и ручного закрытия в хуках. Три необязательных поля webtest.config.mjs
(без них поведение прежнее):
- maxContexts     — потолок одновременно живых сеансов (null = без лимита);
- contextPolicy   — 'reuse' (держать открытыми в пределах лимита) | 'strict'
                    (закрывать non-pinned контексты теста сразу после него);
- pinnedContexts  — не вытесняются LRU (default = [defaultContext]; [] делает
                    default вытесняемым на тесном стенде).

Перед setup каждого теста LRU-вытеснение освобождает слот под нужды теста;
уже открытые нужные контексты переиспользуются. Default больше не вечно-pinned.
Исчерпание пула даёт внятную ошибку вместо маскирующего «Browser not connected».

- new: cli/test-runner/context-pool.mjs — чистый планировщик planEviction + LRU.
- cli/commands/test.mjs (v1.4): парсинг/валидация полей, вытеснение с фолбэк-парковкой
  на нужный контекст (нельзя закрыть единственный активный), strict-закрытие, LRU-трекинг.
- Доки: regression-spec (§7/§8/глоссарий), regression-guide (рецепт), regress.md.
- Регресс дай-фудит фичу: 14-multi-context-routing роутит в 3-й контекст c,
  15-multi-context-handover проверяет вытеснение c на границе; конфиг maxContexts:2.

Юнит-краёв планировщика — в debug/ (gitignored). Live-регресс: 25/25 зелёных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-15 14:57:56 +03:00
co-authored by Claude Opus 4.8
parent 9e1341b80b
commit 3f1168b975
9 changed files with 262 additions and 16 deletions
@@ -1,22 +1,24 @@
export const name = 'Multi-context: routing single test to non-default context';
export const tags = ['multi-context', 'smoke'];
export const context = 'b';
export const context = 'c';
export const timeout = 60000;
export default async function({ getPageState, navigateSection, openCommand, closeForm, assert, step, log }) {
await step('Active context is b', async () => {
// Sanity check — ensure we are routed into b's session
await step('Active context is c', async () => {
// Sanity check — ensure we are routed into c's session (a third, non-default context).
// Leaving c open here is intentional: it becomes the LRU eviction candidate at the
// 14→15 boundary under maxContexts:2 (asserted in 15-multi-context-handover).
const state = await getPageState();
assert.ok(Array.isArray(state.sections) && state.sections.length, 'Sections should be visible');
log('Sections in b: ' + state.sections.map(s => s.name).join(', '));
log('Sections in c: ' + state.sections.map(s => s.name).join(', '));
});
await step('Open Контрагенты in context b', async () => {
await step('Open Контрагенты in context c', async () => {
await navigateSection('Склад');
const state = await openCommand('Контрагенты');
assert.ok(state.form != null, 'List form should open');
log('Opened in b: ' + state.title);
log('Opened in c: ' + state.title);
await closeForm();
});
}
@@ -7,6 +7,19 @@ export default async function({ a, b, assert, step, log }) {
const unique = 'MultiCtx-' + Date.now();
await step('пул: c вытеснен под maxContexts=2 (a,b — ровно два сеанса)', async () => {
// 14-multi-context-routing оставил контекст `c` открытым. Этот тест объявляет [a,b];
// раннер под maxContexts:2 вытесняет LRU-контекст `c`, чтобы открыть `b`. Без пула `c`
// копился бы (открыто было бы a,b,c). Проверяем, что лимит удержан и c закрыт.
// (При запуске 15 в одиночку `c` и не открывался — length===2 всё равно верно.)
const open = await a.listContexts();
log(`пул после setup: [${open.join(',')}]`);
assert.ok(!open.includes('c'), `c должен быть вытеснен под лимитом, открыто: [${open.join(',')}]`);
assert.equal(open.length, 2, `в пуле ровно 2 сеанса, получено ${open.length}: [${open.join(',')}]`);
assert.includes(open, 'a', 'a должен быть открыт');
assert.includes(open, 'b', 'b должен быть открыт');
});
await step('a: открыть Контрагенты, создать новую запись', async () => {
await a.navigateSection('Склад');
await a.openCommand('Контрагенты');
+2 -1
View File
@@ -82,9 +82,10 @@ node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --rebuild-e
## Конфигурация
`tests/web-test/webtest.config.mjs` задаёт:
- **`contexts.a` / `contexts.b`** — два независимых 1C-сеанса (разные cookies) на той же URL. Тесты с `multi-context` тегом используют оба.
- **`contexts.a` / `contexts.b` / `contexts.c`** — независимые 1C-сеансы (разные cookies) на той же URL. `a`,`b` — два «пользователя» мультиконтекст-тестов; `c` задействован только `14-multi-context-routing` и служит топливом для проверки вытеснения пула.
- **`defaultContext: 'a'`** — большинство тестов работают в одном контексте.
- **`isolation: 'tab'`** — вкладки в одном окне (default). Альтернатива `'window'` — отдельный BrowserContext (полная изоляция cookies).
- **`maxContexts: 2` / `contextPolicy: 'reuse'` / `pinnedContexts: []`** — дай-фуд управления пулом лицензий. Лимит 2 одновременных сеанса: на границе `14 (c) → 15 (a,b)` раннер автоматически вытесняет LRU-контекст `c` (проверяется первым шагом `15-multi-context-handover`). Благодаря лимиту 3 контекста никогда не живут одновременно.
## Env переменные
+12
View File
@@ -12,8 +12,20 @@ export default {
// Custom-поля любого типа пробрасываются как есть.
a: { url: 'http://localhost:9191/webtest-runner/ru_RU', displayName: 'Пользователь A' },
b: { url: 'http://localhost:9191/webtest-runner/ru_RU', displayName: 'Пользователь B' },
// c — третий контекст, задействован только 14-multi-context-routing. Под maxContexts:2
// он вытесняется на границе 14→15 (см. проверку пула в 15-multi-context-handover).
c: { url: 'http://localhost:9191/webtest-runner/ru_RU', displayName: 'Пользователь C' },
},
defaultContext: 'a',
// Пул 1С-лицензий (дай-фудим фичу на собственном регрессе). Одновременно живых сеансов —
// не больше maxContexts; LRU-вытеснение освобождает слот под нужды следующего теста.
// pinnedContexts:[] делает default `a` вытесняемым (здесь он всё равно нужен почти всем тестам,
// так что не вытесняется — но снимает жёсткий пин). Благодаря лимиту 3 контекста a/b/c
// никогда не живут одновременно: c закрывается до открытия b.
maxContexts: 2,
contextPolicy: 'reuse',
pinnedContexts: [],
// isolation: 'tab' (default) — persistent context, tabs in one window, 1С extension loads.
// Cookies are shared between tabs but scope by URL path, so different vrd-publications
// give independent auth without extra isolation.