// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * PLAN §12: "At least one Playwright test must exercise the same scenario through multiple * transport/client styles where practical: Playwright HTTP requests, curl, direct binary CLI * execution, and docker exec against the final image" + the matrix line "binary invocation from * the host test environment and invocation through docker exec in the container-image suite". * * One scenario (create, list, authenticated GET, bad-auth GETs, delete, GET → 401, list / list all, * `audit`, audit check in the DB) * runs through five driver combinations and the normalized outcomes must be identical. A second * test proves the host binary and the image operate on the same on-disk format by handing one * database back and forth. */ import { test, expect, AUDIT_COLUMNS, TOKEN_LIST_COLUMNS, assertDbInvariants, buildRawRequest, expectCliSuccess, expectContainersResponse, expectErrorResponse, makeContainerSummary, parseAuditOutput, parseLogs, parseTokenList, readOperations, readTokens, type CliResult, type ContainerSummary, type FakeDocker, type ResponseView, type SecretRegistry, view, } from '../../harness'; import { expectedOrigins } from './image-helpers'; /** The same Engine answer for every driver, so response bodies are comparable byte for byte. */ const FIXED_CONTAINERS: ContainerSummary[] = [ makeContainerSummary(0, undefined, { Names: ['/mt-web'], Labels: { role: 'web', 'unicode.label': 'naïve ☃' } }), makeContainerSummary(1, undefined, { Names: ['/mt-db'] }), makeContainerSummary(2, undefined, { Names: ['/mt-cache'], Command: 'redis-server --save ""' }), ]; const DESCRIPTION = 'multi-transport ☃ "quoted" $HOME `id` ; |'; const UNKNOWN = 'Q'.repeat(43); interface Driver { name: string; tokenNew(description?: string): Promise; /** `token list` (active tokens) or, with all, `token list all`. */ tokenList(all?: boolean): Promise; audit(args?: string[]): Promise; tokenDelete(token: string): Promise; /** GET /api/v1/containers with exactly these Authorization header values (none when empty). */ get(authorization: string[]): Promise; /** Stops the server, returns its exit code + stderr and a readable copy of the database. */ finish(): Promise<{ exitCode: number; stderr: string; dbPath: string }>; origins: string[]; } interface Outcome { cli: Array<{ step: string; exitCode: number | null; stdout: string; stderr: string }>; http: Array<{ step: string; status: number; contentType?: string; wwwAuthenticate?: string; body: string }>; list: Array>; db: { tokens: Array<[string, number, boolean]>; operations: Array<[string, string]> }; serverExit: number; logStatuses: string[]; } /** Runs the scenario through one driver and returns its normalized outcome (tokens replaced by placeholders). */ async function runScenario(d: Driver, secrets: SecretRegistry): Promise { const names = new Map(); const norm = (s: string) => { let out = s; for (const [tok, label] of names) out = out.split(tok).join(label); return out; }; const out: Outcome = { cli: [], http: [], list: [], db: { tokens: [], operations: [] }, serverExit: -1, logStatuses: [] }; const cli = (step: string, r: CliResult, stdout = norm(r.stdout)) => out.cli.push({ step, exitCode: r.exitCode, stdout, stderr: r.stderr }); // tables are compared by cells (strictly parsed, padding re-rendered by the parser); timestamps // and the driver-specific origin replaced by placeholders const tokenTable = (stdout: string) => [TOKEN_LIST_COLUMNS.join('\t'), ...parseTokenList(stdout).map((r) => [norm(r.token), r.descriptionQuoted, String(r.active), '