Перед тем как просить агента менять код, зафиксируйте его текущее поведение характеризационными тестами, а критичные пути — оплату, заявку, права доступа — опишите сценарными тестами с явной таблицей ожиданий. Такие тесты падают, если следующая правка агента «упростит» проверку или молча изменит результат. Чтобы их нельзя было тихо обойти, прогон обязателен в pre-commit и в CI, а пропущенные и пустые тесты ловит линтер.
После материала вы сможете
- написать характеризационный тест через
toMatchInlineSnapshotи прочитать снимок как список решений для владельца продукта - описать критичный путь (оплату, отмену заказа) сценарными тестами, которые проверяют побочные эффекты и путь отказа
- построить матрицу прав «роль × владелец × статус», которая падает при «упрощении» проверки
- сделать прогон обязательным:
npm run checkв pre-commit через husky и в GitHub Actions, плюс правила линтера против.skip,.onlyи тестов безexpect
Что знать заранее
Проверено на
Node.js 24 LTS (24.18)Vitest 5.0.4TypeScript 6.0.3ESLint 10.12, typescript-eslint 8.71, @vitest/eslint-plugin 1.6.27husky 9.1.7actionlint 1.7.12
Сверено с документацией: 12 октября 2026 г.
Содержание
Зачем тесты до следующей правки агента
Ассистент меняет код уверенно и быстро, а помнит о проекте только то, что видит в текущей сессии. Правка в одном месте легко ломает правило в другом: скидку, проверку прав, обработку ошибки оплаты. Ни вы, ни агент не держите в голове все такие правила, и без тестов регрессия обнаруживается уже у пользователей.
Документация Claude Code начинает советы с того же: дайте агенту проверку, которую он может запустить сам, — тесты, сборку, линтер. Без неё сигнал «готово» один — «выглядит готовым», и проверяющим становитесь вы. С ней агент сам запускает проверку, читает результат и исправляет, пока она не пройдёт. Но проверка полезна, только если она проверяет правильные вещи и её нельзя тихо ослабить. Этому и посвящён урок.
Характеризационный
- Что фиксирует
- Текущее поведение функции на наборе входов — каким бы оно ни было
- Что поймает
- Любое незаметное изменение результата, в том числе «заодно» исправленные странности
Сценарный
- Что фиксирует
- Обещание бизнеса: клиента списали один раз, отменённый заказ не оплачивается
- Что поймает
- Правку, после которой путь формально работает, но нарушает правило
Тест-сторож
- Что фиксирует
- Полную таблицу допустимых комбинаций, написанную вручную
- Что поймает
- «Упрощение» условия в проверке прав
Учебный модуль — заказы небольшого интернет-магазина, каким его мог написать ассистент: расчёт суммы с промокодом, правило отмены заказа и оплата через платёжный шлюз. Шлюз в тестах подменяется функцией vi.fn(), реальных платежей и ключей нет.
npm init -y
npm pkg set type=module
npm install -D vitest@5.0.4 typescript@6.0.3 @types/node@24.19.2 \
eslint@10.12.0 typescript-eslint@8.71.1 @vitest/eslint-plugin@1.6.27 husky@9.1.7export type Role = "guest" | "client" | "manager" | "admin";
export type User = { id: string; role: Role };
export type OrderStatus = "new" | "paid" | "cancelled";
export type Order = { id: string; ownerId: string; status: OrderStatus; totalKzt: number };
export type Item = { priceKzt: number; qty: number };
const PROMOS: Record<string, number> = { WELCOME10: 10, VIP25: 25 };
export function calcTotal(items: Item[], promo?: string): number {
const sum = items.reduce((acc, item) => acc + item.priceKzt * item.qty, 0);
const percent = promo ? (PROMOS[promo.trim().toUpperCase()] ?? 0) : 0;
return Math.round((sum * (100 - percent)) / 100);
}
export function canCancel(user: User, order: Order): boolean {
if (order.status !== "new") return false;
if (user.role === "admin" || user.role === "manager") return true;
return user.role === "client" && user.id === order.ownerId;
}import type { Order } from "./orders.js";
export interface PaymentGateway {
charge(orderId: string, amountKzt: number, idempotencyKey: string): Promise<{ paymentId: string }>;
}
export type Deps = { orders: Map<string, Order>; gateway: PaymentGateway };
export async function payOrder(orderId: string, deps: Deps): Promise<Order> {
const order = deps.orders.get(orderId);
if (!order) throw new Error("ORDER_NOT_FOUND");
if (order.status === "paid") return order;
if (order.status !== "new") throw new Error("ORDER_NOT_PAYABLE");
if (order.totalKzt <= 0) throw new Error("INVALID_AMOUNT");
await deps.gateway.charge(order.id, order.totalKzt, `order-${order.id}`);
const paid: Order = { ...order, status: "paid" };
deps.orders.set(order.id, paid);
return paid;
}Характеризационный тест: снимок того, что есть
Характеризационный тест (термин из книги Майкла Фезерса о работе с унаследованным кодом) не спрашивает, правильно ли работает код. Он записывает, как код работает сейчас. Для чужого или сгенерированного кода это честная отправная точка: вы ещё не знаете всех намерений автора, но уже можете заметить любое изменение.
Удобнее всего делать такой тест через встроенный снимок. Соберите интересные входы — обычные, пустые, пограничные, странные — в один объект и вызовите toMatchInlineSnapshot() без аргумента:
import { expect, it } from "vitest";
import { calcTotal } from "../../src/orders.js";
// Characterization test: records what calcTotal does today,
// including behaviour nobody has decided on yet.
it("calcTotal: current behaviour", () => {
const cases = {
"one item": calcTotal([{ priceKzt: 5000, qty: 1 }]),
"two items": calcTotal([{ priceKzt: 5000, qty: 2 }, { priceKzt: 1500, qty: 1 }]),
"empty cart": calcTotal([]),
"promo WELCOME10": calcTotal([{ priceKzt: 5000, qty: 1 }], "WELCOME10"),
"promo lower case + spaces": calcTotal([{ priceKzt: 5000, qty: 1 }], " vip25 "),
"unknown promo": calcTotal([{ priceKzt: 5000, qty: 1 }], "FREE100"),
"rounding 0.5": calcTotal([{ priceKzt: 5, qty: 1 }], "WELCOME10"),
"negative qty": calcTotal([{ priceKzt: 5000, qty: -1 }]),
};
expect(cases).toMatchInlineSnapshot();
});$ npx vitest run
Snapshots 1 written
Test Files 1 passed (1)
Tests 1 passed (1)import { expect, it } from "vitest";
import { calcTotal } from "../../src/orders.js";
// Characterization test: records what calcTotal does today,
// including behaviour nobody has decided on yet.
it("calcTotal: current behaviour", () => {
const cases = {
"one item": calcTotal([{ priceKzt: 5000, qty: 1 }]),
"two items": calcTotal([{ priceKzt: 5000, qty: 2 }, { priceKzt: 1500, qty: 1 }]),
"empty cart": calcTotal([]),
"promo WELCOME10": calcTotal([{ priceKzt: 5000, qty: 1 }], "WELCOME10"),
"promo lower case + spaces": calcTotal([{ priceKzt: 5000, qty: 1 }], " vip25 "),
"unknown promo": calcTotal([{ priceKzt: 5000, qty: 1 }], "FREE100"),
"rounding 0.5": calcTotal([{ priceKzt: 5, qty: 1 }], "WELCOME10"),
"negative qty": calcTotal([{ priceKzt: 5000, qty: -1 }]),
};
expect(cases).toMatchInlineSnapshot(`
{
"empty cart": 0,
"negative qty": -5000,
"one item": 5000,
"promo WELCOME10": 4500,
"promo lower case + spaces": 3750,
"rounding 0.5": 5,
"two items": 11500,
"unknown promo": 5000,
}
`);
});Теперь прочитайте снимок как список решений, которые кто-то принял за вас. Отрицательное количество даёт отрицательную сумму. Неизвестный промокод молча игнорируется. Половина тенге округляется вверх. Возможно, всё это правильно, возможно — нет, но теперь это видно, и решение принимает человек. Странности лучше выписать отдельными задачами, а не чинить «заодно».
$ CI=true npx vitest run
FAIL tests/characterization/calc-total.test.ts > calcTotal: current behaviour
Error: Snapshot `calcTotal: current behaviour 1` mismatched
❯ tests/characterization/calc-total.test.ts:17:17
17| expect(cases).toMatchInlineSnapshot();
| ^
Snapshots 1 failed
Test Files 1 failed (1)Проверим, что тест делает свою работу. Допустим, агента попросили «почистить» расчёт, и он заодно запретил отрицательное количество:
@@ -7,7 +7,7 @@ export type Item = { priceKzt: number; qty: number };
const PROMOS: Record<string, number> = { WELCOME10: 10, VIP25: 25 };
export function calcTotal(items: Item[], promo?: string): number {
- const sum = items.reduce((acc, item) => acc + item.priceKzt * item.qty, 0);
+ const sum = items.reduce((acc, item) => acc + item.priceKzt * Math.max(0, item.qty), 0);
const percent = promo ? (PROMOS[promo.trim().toUpperCase()] ?? 0) : 0;
return Math.round((sum * (100 - percent)) / 100);
}$ npx vitest run tests/characterization
FAIL tests/characterization/calc-total.test.ts > calcTotal: current behaviour
Error: Snapshot `calcTotal: current behaviour 1` mismatched
- Expected
+ Received
@@ -1,8 +1,8 @@
{
"empty cart": 0,
- "negative qty": -5000,
+ "negative qty": 0,
"one item": 5000,
Snapshots 1 failed
Test Files 1 failed (1)Тест не говорит, что новое поведение ошибочно. Он говорит, что поведение изменилось, и останавливает правку до решения человека. Если изменение задумано, снимок обновляют командой npx vitest run -u в том же коммите и объясняют причину в описании pull request. Если нет — откатывают правку.
Сценарные тесты критичных путей
Снимок фиксирует значения, но не объясняет, какие из них важны. Для путей, где ошибка стоит денег или доступа, — оплата, заявка, права — нужны тесты, названные языком бизнеса: «повторная оплата не списывает деньги второй раз», «при сбое шлюза заказ остаётся неоплаченным». Такой тест читается как требование, и его не стоит переписывать ради прохождения.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Order } from "../../src/orders.js";
import { payOrder, type Deps } from "../../src/payments.js";
function setup(order: Order) {
const charge = vi.fn(async () => ({ paymentId: "pay_test_1" }));
const deps: Deps = { orders: new Map([[order.id, order]]), gateway: { charge } };
return { deps, charge };
}
describe("payment of an order", () => {
let order: Order;
beforeEach(() => {
order = { id: "o-1", ownerId: "u-1", status: "new", totalKzt: 4500 };
});
it("charges the exact total once and marks the order as paid", async () => {
const { deps, charge } = setup(order);
const paid = await payOrder("o-1", deps);
expect(paid.status).toBe("paid");
expect(charge).toHaveBeenCalledTimes(1);
expect(charge).toHaveBeenCalledWith("o-1", 4500, "order-o-1");
});
it("a repeated payment does not charge the customer twice", async () => {
const { deps, charge } = setup(order);
await payOrder("o-1", deps);
await payOrder("o-1", deps);
expect(charge).toHaveBeenCalledTimes(1);
});
it("a cancelled order cannot be paid", async () => {
const { deps, charge } = setup({ ...order, status: "cancelled" });
await expect(payOrder("o-1", deps)).rejects.toThrow("ORDER_NOT_PAYABLE");
expect(charge).not.toHaveBeenCalled();
});
it("an order with a zero or negative total is never sent to the gateway", async () => {
const { deps, charge } = setup({ ...order, totalKzt: -5000 });
await expect(payOrder("o-1", deps)).rejects.toThrow("INVALID_AMOUNT");
expect(charge).not.toHaveBeenCalled();
});
it("the order stays unpaid if the gateway fails", async () => {
const { deps, charge } = setup(order);
charge.mockRejectedValueOnce(new Error("GATEWAY_TIMEOUT"));
await expect(payOrder("o-1", deps)).rejects.toThrow("GATEWAY_TIMEOUT");
expect(deps.orders.get("o-1")?.status).toBe("new");
});
});- Проверяйте побочные эффекты, а не только результат: сколько раз вызван шлюз и с какими аргументами.
- Обязательно описывайте путь отказа: отменённый заказ, нулевая сумма, ошибка внешнего сервиса. Сгенерированный код чаще всего ошибается именно там.
- Ключ идемпотентности (
order-o-1) тоже часть контракта: шлюз по нему отличает повтор от новой оплаты. - Каждый тест готовит свои данные: функция
setupсоздаёт свежий заказ и свою подделку шлюза.
Типичная правка ассистента, которую ловит последний тест: попросили «сделать оплату устойчивее», и ошибку шлюза обернули в try/catch с записью в лог. Код стал «надёжнее», а заказ после сбоя отмечается оплаченным, хотя денег нет:
@@ -13,7 +13,11 @@ export async function payOrder(orderId: string, deps: Deps): Promise<Order> {
if (order.status !== "new") throw new Error("ORDER_NOT_PAYABLE");
if (order.totalKzt <= 0) throw new Error("INVALID_AMOUNT");
- await deps.gateway.charge(order.id, order.totalKzt, `order-${order.id}`);
+ try {
+ await deps.gateway.charge(order.id, order.totalKzt, `order-${order.id}`);
+ } catch (error) {
+ console.warn("payment failed, will retry later", error);
+ }
const paid: Order = { ...order, status: "paid" };
deps.orders.set(order.id, paid);
return paid;$ npx vitest run tests/critical
❯ tests/critical/payment.test.ts (5 tests | 1 failed)
× the order stays unpaid if the gateway fails
FAIL tests/critical/payment.test.ts > payment of an order > the order stays unpaid if the gateway fails
AssertionError: promise resolved "{ id: 'o-1', ownerId: 'u-1', …(2) }" instead of rejecting
+ "status": "paid",
Test Files 1 failed | 1 passed (2)
Tests 1 failed | 29 passed (30)Тест-сторож: полная матрица прав
Проверки прав — любимое место для «упрощений»: три условия выглядят избыточными, и их хочется свернуть в одно. Защита — перебрать все комбинации роли, владения и статуса и для каждой сравнить ответ с таблицей, написанной вручную.
import { describe, expect, it } from "vitest";
import { canCancel, type Order, type OrderStatus, type Role } from "../../src/orders.js";
// The rule written out by hand, not derived from the code:
// only these combinations may cancel an order. Everything else is forbidden.
const ALLOWED = new Set([
"client/own/new",
"manager/own/new",
"manager/foreign/new",
"admin/own/new",
"admin/foreign/new",
]);
const roles: Role[] = ["guest", "client", "manager", "admin"];
const statuses: OrderStatus[] = ["new", "paid", "cancelled"];
const matrix = roles.flatMap((role) =>
(["own", "foreign"] as const).flatMap((owner) =>
statuses.map((status) => ({ role, owner, status, allowed: ALLOWED.has(`${role}/${owner}/${status}`) })),
),
);
describe("who can cancel an order", () => {
it("the matrix covers every combination", () => {
expect(matrix).toHaveLength(roles.length * 2 * statuses.length);
});
it.each(matrix)("$role, $owner order, $status → $allowed", ({ role, owner, status, allowed }) => {
const order: Order = { id: "o-1", ownerId: "u-1", status, totalKzt: 5000 };
const user = { id: owner === "own" ? "u-1" : "u-2", role };
expect(canCancel(user, order)).toBe(allowed);
});
});Важная деталь: ожидание ALLOWED не вычисляется той же логикой, что в canCancel. Если скопировать условие из кода в тест, тест будет согласен с любой ошибкой — он проверит код самим собой. Список разрешённых комбинаций — это требование, его утверждает владелец продукта.
Теперь «упрощение», которое легко пропустить на ревью: условие стало короче и выглядит логичным.
@@ -13,7 +13,5 @@ export function calcTotal(items: Item[], promo?: string): number {
}
export function canCancel(user: User, order: Order): boolean {
- if (order.status !== "new") return false;
- if (user.role === "admin" || user.role === "manager") return true;
- return user.role === "client" && user.id === order.ownerId;
+ return order.status === "new" && user.role !== "guest";
}$ npx vitest run tests/critical
❯ tests/critical/permissions.test.ts (25 tests | 1 failed)
❯ who can cancel an order (25)
× client, foreign order, new → false
FAIL tests/critical/permissions.test.ts > who can cancel an order > client, foreign order, new → false
AssertionError: expected true to be false // Object.is equality
Test Files 1 failed | 1 passed (2)
Tests 1 failed | 29 passed (30)Имя упавшего теста — уже готовое описание дыры: «клиент, чужой заказ, статус new». Без матрицы такая правка проходит и ручную проверку, и тесты «счастливого пути»: свой заказ клиент по-прежнему отменяет, а гость по-прежнему не может.
Чтобы тесты нельзя было тихо ослабить
Когда проверка не проходит, у агента, как и у уставшего человека, есть короткий путь: пропустить тест, убрать неудобный expect или запустить только «свой» тест через .only. Прогон после этого зелёный:
@@ -25,12 +25,10 @@ describe("payment of an order", () => {
});
it("a repeated payment does not charge the customer twice", async () => {
- const { deps, charge } = setup(order);
+ const { deps } = setup(order);
await payOrder("o-1", deps);
await payOrder("o-1", deps);
-
- expect(charge).toHaveBeenCalledTimes(1);
});
it("a cancelled order cannot be paid", async () => {
@@ -47,7 +45,7 @@ describe("payment of an order", () => {
expect(charge).not.toHaveBeenCalled();
});
- it("the order stays unpaid if the gateway fails", async () => {
+ it.skip("the order stays unpaid if the gateway fails", async () => {
const { deps, charge } = setup(order);
charge.mockRejectedValueOnce(new Error("GATEWAY_TIMEOUT"));$ npm test
Test Files 3 passed (3)
Tests 30 passed | 1 skipped (31)
$ npm run lint
tests/critical/payment.test.ts
27:3 error Test has no assertions vitest/expect-expect
48:6 error Disabled test - if you want to skip a test temporarily, use .todo() instead vitest/no-disabled-tests
✖ 2 problems (2 errors, 0 warnings)Ловят это три механизма. Правила @vitest/eslint-plugin запрещают пропущенные тесты, .only и тесты без единой проверки. Параметр allowOnly: false в конфигурации Vitest роняет прогон при .only и локально, хотя по умолчанию .only запрещён только в CI.
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import vitest from "@vitest/eslint-plugin";
export default defineConfig(
tseslint.configs.recommended,
{
files: ["tests/**/*.ts"],
plugins: { vitest },
rules: {
...vitest.configs.recommended.rules,
// a skipped or focused test must not reach the main branch
"vitest/no-disabled-tests": "error",
"vitest/no-focused-tests": "error",
// a test without a single expect proves nothing
"vitest/expect-expect": "error",
},
},
);import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["tests/**/*.test.ts"],
allowOnly: false,
},
});$ npx vitest run
FAIL tests/critical/payment.test.ts > payment of an order > a cancelled order cannot be paid
Error: [Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error
Test Files 1 failed | 2 passed (3)Третий механизм — организационный. Правила для агента записывают в файл инструкций проекта (как его устроить, рассказано в материале про AI-ассистентов в разработке), а изменения в критичных тестах требуют одобрения ответственного: GitHub автоматически запрашивает ревью у владельцев путей из CODEOWNERS, а настройка защиты ветки «Require review from Code Owners» делает это одобрение обязательным.
# AGENTS.md
## Tests
- Run `npm run check` before reporting a task as done and show the output.
- For a bug, first write a test that fails, then fix the code.
- Tests in `tests/critical/` describe payment and access rules.
Do not edit, skip or delete them without asking.
- A failing snapshot means behaviour changed. Report the diff;
never run `vitest -u` to make the run green.
- Never add `.skip`, `.only` or remove `expect` calls to pass a check.# Payment and access rules need a human review
/tests/critical/ @your-org/maintainers
/tests/characterization/ @your-org/maintainersОбязательный прогон: pre-commit и CI
Тесты, которые запускают «когда вспомнят», не защищают. Соберите все проверки в одну команду и запускайте её в двух местах: перед коммитом на машине разработчика и в CI на каждый pull request.
{
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"check": "npm run typecheck && npm run lint && npm test",
"prepare": "husky"
}
}npm run check$ git commit -m "Simplify canCancel"
> vitest run
❯ tests/critical/permissions.test.ts (25 tests | 1 failed)
× client, foreign order, new → false
Test Files 1 failed | 2 passed (3)
Tests 1 failed | 30 passed (31)
husky - pre-commit script failed (code 1)
exit code: 1
$ git commit --no-verify -m "Simplify canCancel"
[main 48bc214] Simplify canCancel
1 file changed, 1 insertion(+), 3 deletions(-)
exit code: 0Второй коммит показывает границу pre-commit: по документации Git хук пропускается флагом --no-verify, а husky отключается переменной HUSKY=0. Хук экономит время и ловит ошибку рано, но обязательной проверкой он быть не может. Обязательная — в CI:
name: check
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run check$ docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12 -verbose .github/workflows/check.yml
verbose: Found 0 parse errors in 0 ms for .github/workflows/check.yml
verbose: Found total 0 errors in 22 ms for .github/workflows/check.ymlВключите защиту ветки
В настройках репозитория для
mainвключите «Require status checks to pass before merging» и выберите проверкуcheck. По документации GitHub после этого слить изменения можно только при успешной проверке.Не давайте обходить правило
По умолчанию ограничения не действуют на администраторов. Включите «Do not allow bypassing the above settings», если проект ведут один-два человека и соблазн нажать «Merge» в обход велик.
Требуйте ревью владельцев
«Require review from Code Owners» делает обязательным одобрение изменений в
tests/critical/. Подробности о workflow, кэше и матрице версий — в уроке по CI/CD на GitHub Actions.
Ограничения и упражнения
- Тесты проверяют только то, что в них записано. Матрица прав не поймает дыру в другом эндпоинте, где проверки нет вовсе.
- Покрытие строк не равно корректности: тест без проверки покрывает код и ничего не доказывает. Поэтому правило
expect-expectважнее процента покрытия. - Большие снимки превращаются в шум, и их начинают обновлять не читая. Снимайте компактные значения, а не целые объекты ответа.
- Тесты, написанные тем же агентом, что и код, могут повторять его ошибку. Документация Claude Code предлагает разделять роли: одна сессия пишет тесты, другая — код под них. Критичные тесты в любом случае читает человек.
- Следующий шаг — мутационное тестирование (например, StrykerJS): инструмент сам вносит в код мелкие «упрощения» и проверяет, что тесты их ловят.
Как проверено7 · пройдено1 · частично1 · не запускалосьсверено 12 октября 2026 г.
- запущено, работает
src/orders.ts, src/payments.ts, tests/characterization/calc-total.test.ts (toMatchInlineSnapshot)
временный проект, Node.js 24.18, Vitest 5.0.4, TypeScript 6.0.3: первый vitest run записал inline-снимок (Snapshots 1 written); тот же тест без снимка при CI=true упал (Snapshot mismatched); логи run1, run2
- запущено, работает
tests/critical/payment.test.ts, tests/critical/permissions.test.ts
vitest run: 3 файла, 31 тест, все прошли; tsc --noEmit без ошибок; шлюз оплаты — vi.fn(), реальных платежей нет
- запущено, работает
регрессии «правок агента» (regression1–3.diff)
каждая правка внесена в код и прогнана: упрощение canCancel → падает «client, foreign order, new → false»; запрет отрицательного qty → diff снимка; try/catch вокруг charge → падает «the order stays unpaid if the gateway fails»; логи run4–run6, после каждой проверки код возвращён
- запущено, работает
eslint.config.js (@vitest/eslint-plugin), vitest.config.ts (allowOnly: false)
ESLint 10.12 + typescript-eslint 8.71 + @vitest/eslint-plugin 1.6.27: ослабленный тест (it.skip и удалённый expect) — npm test зелёный (30 passed | 1 skipped), eslint — 2 ошибки expect-expect / no-disabled-tests; it.only — vitest падает «Unexpected .only modifier»; логи run8, run9
- запущено, работает
package.json scripts.check, .husky/pre-commit (husky 9.1.7)
временный git-репозиторий: коммит со сломанным canCancel остановлен хуком (husky - pre-commit script failed (code 1)), с --no-verify прошёл; лог run10, run11
- запущено, работает
AGENTS.md (раздел о тестах)
markdownlint-cli2 0.23.3: 0 issues
- запущено, работает
решение упражнения «роль courier»
сначала изменён ALLOWED — упали 2 теста courier/paid; после изменения canCancel — 31 тест прошёл (30 комбинаций + проверка размера), tsc без ошибок; лог run15
- частично
.github/workflows/check.yml
actionlint 1.7.12 (docker rhysd/actionlint, --rm): 0 ошибок; шаги workflow повторены в чистом git clone: npm ci && CI=true npm run check — 31 тест прошёл; на GitHub-раннере не запускался (нет репозитория)
- не запускалось
.github/CODEOWNERS
синтаксис сверен с GitHub Docs (About code owners); на реальном репозитории с защитой ветки не проверялся
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.