[AZ-474] [AZ-480] Batch 8 - tile-split + nginx/image static checks (Phase A close)

- AZ-474 tile-split + YOLO parser + auto-zoom + indicator +
  malformed (FT-P-51..55, FT-N-10): 13 fast (6 it.fails for
  AC-1..6 + 7 controls) + 2 e2e (test.fail for FT-P-51 +
  FT-P-53). The split surface is QUARANTINED today (D11) —
  no Split-tile button, no parser, no <TileViewer>; all 6
  ACs are documented drift, every it.fails paired with a
  control PASS pinning current behaviour.
- AZ-480 prod image + nginx routing + RAM (NFT-RES-LIM-02
  /03/08/09/10): 4 new static checks promoted into the
  per-commit profile (STC-RES02 500M cap, STC-RES03
  Dockerfile final-stage nginx:alpine no Node, STC-RES09
  exactly 9 /api/* location blocks, STC-RES10 prefix-strip
  on every route). 3 e2e (docker-no-Node probe, runtime
  prefix-strip, long-running RAM soak — all gated on docker
  availability + image build; RAM soak also on
  RUN_LONG_RUNNING=1).

Phase A — One-time baseline setup is now COMPLETE. The
todo/ directory is empty after this batch's archival.
Cumulative review for batches 07-08 is the next autodev
action; after that, Step 7 (Run Tests) auto-chains.

Code review: PASS (0 findings). Fast: 26/26 files, 163
passed / 13 skipped. Static: 29/29 PASS (incl. 4 new
STC-RES* gates).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 06:12:29 +03:00
parent cdebfccada
commit f2451944fd
9 changed files with 1196 additions and 1 deletions
+117
View File
@@ -0,0 +1,117 @@
import { test, expect } from '@playwright/test'
// AZ-474 — e2e companion for FT-P-51 (tile-split endpoint contract) and
// FT-P-53 (DatasetItem.isSplit honored).
//
// Per the task spec, only FT-P-51 and FT-P-53 are `fast + e2e`. The other
// rows (FT-P-52 parser, FT-P-54 auto-zoom, FT-P-55 indicator, FT-N-10
// malformed) are fast-only. Both e2e tests are `test.fail()` today
// because the split surface is QUARANTINED in production (per
// `_docs/04_refactoring/01-testability-refactoring/deferred_to_refactor.md`
// row D11 and the traceability matrix's `[Q]` marker on AC-39).
//
// Once the SPA wires a "Split tile" affordance and starts honoring
// `DatasetItem.isSplit`, remove the `test.fail` and these flip green.
test.describe('AZ-474 — tile-split surface (e2e companion)', () => {
test.fail(
'FT-P-51 — clicking Split tile POSTs /api/annotations/dataset/<id>/split',
async ({ page }) => {
test.setTimeout(20_000)
const splitPosts: string[] = []
await page.route('**/api/annotations/dataset/*/split', async (route) => {
if (route.request().method() === 'POST') {
splitPosts.push(route.request().url())
}
await route.continue()
})
await page.goto('/dataset')
// Suite seed must include at least one dataset item — if not, mark
// the gap explicitly. The seed today produces images via the
// annotations service; if it doesn't, the test reports the seed gap
// and skips rather than hiding the contract.
const firstCard = page.locator('img').first()
if (!(await firstCard.isVisible({ timeout: 5000 }).catch(() => false))) {
test.skip(true, 'Suite seed has no dataset items')
}
await firstCard.hover()
// Drift today: no Split-tile button is rendered. The locator below
// is intentionally tolerant of any reasonable button shape so that
// when the affordance lands, the test does not need surgery.
const splitBtn = page.getByRole('button', { name: /split/i })
await expect(splitBtn.first()).toBeVisible({ timeout: 1500 })
await splitBtn.first().click()
await expect.poll(() => splitPosts.length, { timeout: 2000 }).toBeGreaterThan(0)
expect(splitPosts[0]).toMatch(/\/api\/annotations\/dataset\/[^/]+\/split$/)
},
)
test.fail(
'FT-P-53 — items with isSplit:true render a distinct affordance vs non-split',
async ({ page }) => {
test.setTimeout(15_000)
// Stub the dataset response so the test is independent of seed
// shape — what matters is the renderer's behaviour given the
// contract, not which rows happen to live in the suite seed.
await page.route('**/api/annotations/dataset*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [
{
annotationId: 'ann-split',
imageName: 'split.jpg',
thumbnailPath: '/thumbs/split.jpg',
status: 20,
createdDate: '2026-05-11T10:00:00Z',
createdEmail: 'op_alice@test.local',
flightName: 'Flight 1',
source: 0,
isSeed: false,
isSplit: true,
},
{
annotationId: 'ann-nosplit',
imageName: 'nosplit.jpg',
thumbnailPath: '/thumbs/nosplit.jpg',
status: 10,
createdDate: '2026-05-11T10:01:00Z',
createdEmail: 'op_alice@test.local',
flightName: 'Flight 1',
source: 1,
isSeed: false,
isSplit: false,
},
],
totalCount: 2,
}),
})
})
await page.goto('/dataset')
await expect(page.locator('img').first()).toBeVisible({ timeout: 5_000 })
// Spec: the rendered card for an isSplit annotation MUST carry a
// visible affordance the non-split card does NOT carry.
const splitCard = page.locator('img[alt*="split"]').first()
const nonSplitCard = page.locator('img[alt*="nosplit"]').first()
const splitData = await splitCard.evaluate((n) =>
(n.closest('div') as HTMLElement | null)?.getAttribute('data-is-split'),
)
const nonSplitData = await nonSplitCard.evaluate((n) =>
(n.closest('div') as HTMLElement | null)?.getAttribute('data-is-split'),
)
expect(splitData).toBe('true')
expect(nonSplitData).not.toBe('true')
},
)
})