test(cv-qa): prove connection-loss rollback and bounded parser safety
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System.Security.Claims;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.MariaDb.Tests;
|
||||
|
||||
public class CvImportConnectionLossTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Connection_loss_after_all_writes_before_commit_rolls_back_and_exact_retry_is_durable()
|
||||
{
|
||||
await using var database = await MariaDbDatabase.CreateAsync();
|
||||
const string owner = "rd21-owner";
|
||||
int runId;
|
||||
await using (var seed = database.Context())
|
||||
{
|
||||
seed.Users.Add(new() { Id = owner, UserName = "connection-loss-synthetic" });
|
||||
var run = new CvExtractionRun { OwnerUserId = owner, Trigger = "document_import", ParserVersion = ProfileCvController.DocumentParserVersion,
|
||||
Status = "pending_review", StartedAtUtc = DateTimeOffset.UtcNow, RawExtractedText = "C#", DocumentJson = "{}",
|
||||
StructuredProfileJson = StructuredCvProfileJson.SerializePersisted(new() { Skills = { "C#" } }) };
|
||||
seed.CvExtractionRuns.Add(run);
|
||||
await seed.SaveChangesAsync();
|
||||
runId = run.Id;
|
||||
}
|
||||
bool killed = false;
|
||||
ProfileCvController Controller(JobTrackerContext db, bool interrupt)
|
||||
{
|
||||
var user = db.Users.Single(u => u.Id == owner);
|
||||
var users = TestHostFactory.CreateUserManager();
|
||||
users.Setup(u => u.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
users.Setup(u => u.UpdateAsync(user)).Returns(async () =>
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
if (interrupt)
|
||||
{
|
||||
var connection = Assert.IsType<MySqlConnector.MySqlConnection>(db.Database.GetDbConnection());
|
||||
var thread = connection.ServerThread;
|
||||
Assert.True(thread > 0);
|
||||
await using var killer = database.Context();
|
||||
// Only the connection opened by this guarded disposable database fixture is terminated.
|
||||
await killer.Database.ExecuteSqlRawAsync("KILL CONNECTION " + thread.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
killed = true;
|
||||
}
|
||||
return IdentityResult.Success;
|
||||
});
|
||||
return new(users.Object, Mock.Of<ISummarizerService>(), db, null!)
|
||||
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
|
||||
}
|
||||
var request = new ProfileCvController.ApplyReviewedRequest(0, new[] { new CvImportDecision("skills.0", "use") });
|
||||
await using (var broken = database.Context())
|
||||
{
|
||||
using var lifetime = new CancellationTokenSource();
|
||||
var controller = Controller(broken, true);
|
||||
controller.HttpContext.RequestAborted = lifetime.Token;
|
||||
var applying = controller.ApplyReviewed(runId, request);
|
||||
var failure = await Record.ExceptionAsync(() => applying.WaitAsync(TimeSpan.FromSeconds(15)));
|
||||
if (failure is TimeoutException)
|
||||
{
|
||||
lifetime.Cancel();
|
||||
await Record.ExceptionAsync(() => applying.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
Assert.True(killed);
|
||||
Assert.NotNull(failure);
|
||||
Assert.IsNotType<TimeoutException>(failure);
|
||||
}
|
||||
await using (var verify = database.Context())
|
||||
{
|
||||
var run = await verify.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal("pending_review", run.Status);
|
||||
Assert.Null(run.ReviewJson);
|
||||
Assert.Empty(await verify.CareerProfileVersions.ToListAsync());
|
||||
Assert.Empty(await verify.CareerSkills.ToListAsync());
|
||||
Assert.Null((await verify.Users.SingleAsync()).CurrentCvExtractionRunId);
|
||||
var snapshot = await new CareerProfileService(verify).LoadSnapshotAsync(owner, default);
|
||||
Assert.Equal(0, snapshot.Revision);
|
||||
Assert.Empty(snapshot.Profile.Skills);
|
||||
Assert.IsType<OkObjectResult>(await Controller(verify, false).ApplyReviewed(runId, request));
|
||||
}
|
||||
await using var replay = database.Context();
|
||||
Assert.IsType<OkObjectResult>(await Controller(replay, false).ApplyReviewed(runId, request));
|
||||
Assert.Single(await replay.CareerProfileVersions.ToListAsync());
|
||||
Assert.Equal("C#", (await replay.CareerSkills.SingleAsync()).Name);
|
||||
Assert.Equal("applied", (await replay.CvExtractionRuns.SingleAsync()).Status);
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,10 @@ The run status is now an EF concurrency token, with matching snapshot metadata a
|
||||
|
||||
The GREEN cases confirm no canonical write after discard, no discard after apply, retained applied receipts and exact replay after retention, terminal cancellation preservation and rejected terminal retries. Existing owner/tenant and first/existing-profile revision tests remain enabled. A displayed success still requires confirmed durable application.
|
||||
|
||||
## Final actual connection-loss qualification
|
||||
|
||||
The final current-source lane passes **80/80**, zero skips (`mariadb-final-01/mariadb.trx`); startup, locked restore, test and owned-container cleanup exit0. The additional test terminates only its own active connection's numeric `ServerThread` in the guarded disposable database, after all writes but before outer commit. A returned success or a hung request cannot qualify: it requires a real failure, rejects timeout, checks revision0/pending review/no receipt/no skill or history/unchanged Identity pointer, then verifies exact retry and replay produce one durable revision and one skill. This is actual MariaDB session loss, not a mocked provider exception or a whole-server/power-loss experiment. No production connection is used.
|
||||
|
||||
The extra test is a separately bound test-only addition after the full application freeze; all original source hashes remain unchanged. The final provider run includes the seven lifecycle cases and all earlier mapping/rollback/owner/revision cases. A separate warmed sample reports **51.055ms** for reviewed application including validation, merge, transaction and Identity projection; it is not a latency SLA.
|
||||
|
||||
This qualifies the exercised disposable MariaDB behavior. It does not establish hosted CI, production database behavior, power-loss recovery or server filesystem/backup integrity. Historical RD21 evidence is preserved; current-source focused races complement rather than replace its full regression lanes.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Performance and resilience — 2026-10-27
|
||||
|
||||
These are bounded local synthetic measurements, not production capacity or a model benchmark. CPU/GPU/server assumptions from the original task were not re-probed on production.
|
||||
|
||||
Thirty-six real sequential PDF/DOCX parser children (`structural-extraction-01/resources.json`) completed. Wall time median **2.335s**, maximum **5.653s**; CPU median **1.930s**, maximum **3.813s**. Peak Windows working set median **142,776,320 bytes**, maximum **146,653,184 bytes**. Inputs totalled **1,058,593 bytes**, structured outputs **31,664 bytes**. Working-set observations do not establish Windows kernel memory enforcement. Output bytes do not measure peak temporary disk allocation.
|
||||
|
||||
The current C# harness records interpretation-call timings separately in each structural metric case. PDF median **0.854ms**, maximum **65.587ms**; DOCX median **0.554ms**, maximum **67.034ms** (18 cases each). These include process/JIT effects and are not parser conversion times. Upload validation, normalization/validation and review preparation do not have independently qualified span timings; browser upload-to-ready includes those stages. No invented per-stage latency or percentile is supplied.
|
||||
|
||||
`sidecar-load-01/results.json` records two simultaneous requests to the owned loopback read-only test container, with a 1 GiB memory/two CPU/128 PID/512 MiB tmpfs limit. The admitted DOCX returned 200 with 13 blocks in **2.921s**. The concurrent request returned 503 busy in **0.109s**, demonstrating the one-child bound. Before and after, `/tmp` reported zero KiB and zero `work-*` directories. Post-run container memory was 356.8 MiB; this is a snapshot, not peak memory. No sustained stress, production saturation or denial-of-service test was run.
|
||||
|
||||
Real-browser attachments and `journey-latencies.json` retain actual sampled persisted stages and per-journey timing. The four first uploads reached pending review in **1,983–3,024ms**; approved application through independent durable readback took **173–198ms**; editor open/edit-to-preview **2,584–2,799ms**; PDF export-to-saved-bytes **1,122–1,264ms**. These browser cases use the isolated application test database and real sidecar; they are not MariaDB latency benchmarks. Reconfirmed target application took 133ms; exact-source replay/readback 178ms. User review time includes automation and is not human correction burden. Database transaction correctness is tested separately with finite MariaDB concurrency barriers and rollback injection; no production outage or storage power-loss experiment was run. Parser timeouts and malformed inputs are covered separately by the Python tests; no fabricated timer marks a job complete.
|
||||
|
||||
Limits remain the existing upload/page/archive/text caps, one parser child, bounded subprocess deadline/CPU/POSIX memory and backend operation admission/recovery. Cancellation and terminal-state protection prevent stale retry/retention from reopening accepted or discarded runs. Future capacity qualification should use an approved isolated host representative of production, sustained bounded mixed-document queues, peak CPU/RSS/tmpfs sampling, cancellation/restart/outage recovery and database latency percentiles. Real model timings remain **BLOCKED** until immutable-artifact qualification.
|
||||
|
||||
The final real MariaDB80-case run records one warmed reviewed-application sample of **51.055ms**, including validation, merge, transaction and Identity projection (`ReviewedImportApplyMs` in `mariadb-final-01/mariadb.trx`). The new actual connection-loss test confirms rollback and exact retry/replay rather than a timeout being treated as recovery. The long real-Docling/browser fixture completes in **33.4s** for source upload, explicit fixture approval/readback and three complete PDF exports; this is an automated task duration, not measured human effort. All three exports contain four pages and all expected facts/associations; no live model or production load was involved.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Security and privacy — 2026-10-27
|
||||
|
||||
Decision: **CONDITIONAL** for isolated development; production **NO-GO**. Review uses the repository security-auditor workflow and Codex Security static diff review, with separate actual test evidence. No production Docker, MariaDB, filesystem, model cache or service was changed.
|
||||
|
||||
## Changed-source review
|
||||
|
||||
Codex Security scan `9196450b-d1cb-4625-ae79-039c210ce881` completed and sealed for immutable `ef425c2..89974a2`: **zero findings**, 22 host inventory entries plus 13 supplementary changed paths, 35 total, no deleted paths or deferred review tasks. Canonical threat model, coverage, report and SARIF are retained under `C:/Users/Connor/.codex/state/plugins/codex-security/scans/Job-tracker/89974a257e4192b3d93779000a7df6998afc4051_20261026T232811Z_cjmnex0o/`. The newer checkout includes subsequent documentation; scanned source was read immutably. A sealed per-file note for `ProfileCvController.cs` mistakenly describes translations; the actual lifecycle diff was inspected and the canonical threat model correctly describes it. No claim depends on that note.
|
||||
|
||||
All three capability preflight checks passed; no user configuration was changed. The account advisory reported Daybreak not granted, which may limit display of protected results and did not gate the scan. Workbench usage reports 9,039,853 total / 9,016,404 input / 8,725,888 cached input tokens across three threads (`codex_rollout`); this is host-attributed usage, **not isolated incremental scan cost**. Static review cannot prove runtime exploitation resistance, deployment isolation or production readiness.
|
||||
|
||||
After that immutable scan, four test/config/fixture-generator files were independently reviewed. Three assertion gaps were tightened before execution: timeout cannot count as connection-failure success, details must belong to each correct job, and effective template/page settings must match. Actual provider80, the long real-browser case, fresh frontend995/109, TypeScript, lint and independent PDF oracles passed. These additions change no application behavior; they are not misrepresented as part of the sealed35-path tool scan.
|
||||
|
||||
## Boundaries and actual checks
|
||||
|
||||
| Boundary | Current protection and verification |
|
||||
|---|---|
|
||||
| Upload type/size/spoofing | Existing authenticated admission, signature/extension validation, 5 MiB and page/archive limits, duplicate/pending caps retained; full backend and parser security suites remain enabled. |
|
||||
| PDF/DOCX active content | Existing external/active payload, DTD/resolver, archive expansion/ratio/path and malicious PDF rejection retained. Real malformed/truncated/native document tests rerun; arbitrary document safety is not guaranteed. |
|
||||
| Parser resource/privacy | One parser child, private temporary environment, bounded time/CPU and POSIX resources, offline network/process restrictions, read-only isolated container. Native parser dependencies remain a patching obligation. |
|
||||
| Mapping/model output | Owned source block required, category/field/target/count/value allowlists, whole-token source matching and explicit corrections. Caller cannot author provenance/IDs. Local model seam remains disabled; synthetic schema/adversarial tests do not establish real inference quality. |
|
||||
| Owner and tenant | Run/artifact/review/source/profile lookup remains authenticated owner-scoped. Actual browser strangers receive 404 for source/review; no caller tenant ID controls lookup. |
|
||||
| Revision and terminal races | Existing atomic canonical revision/history/projection plus status concurrency, narrow MariaDB 1020 handling, bounded retention reload and terminal retry/cancel guards; real MariaDB race/rollback/replay evidence in MARIA_DB_AND_RD21.md. |
|
||||
| Original/source access | Owned private path, `no-store`, browser source hash equals uploaded bytes. Extracted text is untrusted display content. |
|
||||
| Cleanup | Two simultaneous synthetic sidecar requests returned one 200 and one 503 busy; `/tmp` had zero work directories before and after. Retention/cancellation/orphan and failure tests remain enabled; this bounded check is not a sustained abuse/load test. |
|
||||
|
||||
The isolated test sidecars used immutable image `sha256:8a29cb90fcc2817b0d784826d55a172c943f5ad7e1b6105e690a2d79ee15fe87`, loopback-only, read-only, 1 GiB memory, two CPU quota, 128 PIDs, 512 MiB temporary filesystem. Both owned containers were removed and their exact identities verified absent. Linux API/database/schema/network cleanup and final provider cleanup also passed. Windows extraction working set is measured; the parser's memory argument is not a kernel memory ceiling on Windows. The Python audit hook is defense in depth, not a native sandbox.
|
||||
|
||||
Actual MariaDB session termination after writes/before commit is covered by the final80-case lane: no success acknowledgement, pending/no-receipt state and empty canonical facts/history after rollback, followed by one durable retry/replay. This closes that narrow interruption test; it does not qualify whole-server outage, storage power loss or production recovery.
|
||||
|
||||
Original files/raw extraction/block JSON expire after seven days, or one day for failed/cancelled/discarded work, plus up to the hourly sweep interval while running. Startup cleanup catches overdue work. Deletion failures retain references for retry. Applied candidates, including unselected extracted evidence, audit decisions/receipts and canonical history persist under existing account deletion/retention rules; original-source expiry does not erase this history. Privacy/operator review must account for that distinction and backup retention.
|
||||
|
||||
## Current advisories and inherited gates
|
||||
|
||||
Fresh independent checks in `.artifacts/cv-integration-qa/independent-02/`: npm audited 881 dependencies with zero reported vulnerabilities; the CV-import Python lock audited 48 dependencies with zero reported advisories/skips. `dotnet-audit-02.json` lists no vulnerable API packages including transitive packages. All native commands exited zero. These are point-in-time advisory checks, not proof against undisclosed flaws. The first NuGet wrapper attempt used an unsupported flag and is retained separately; only the corrected native command establishes this result.
|
||||
|
||||
Latest authoritative P9-SEC-02 records distinguish 99 fixed/version-verified dispositions and 10 scoped exclusions from the original109, with Python dependency security **CONDITIONAL PASS**. Its trained-model/cache compatibility PASS is scoped to the immutable BART artifact. The installed dependency scan still remained incomplete for vendor CPU `torch==2.13.0+cpu`, absent from the queried PyPI record; current slim-lock auditing does not close that gap. These historical findings are neither all unresolved nor globally cleared. No new exception or owner risk acceptance is granted here. Hosted MariaDB isolation, deployed identity, production filesystem integrity, off-disk backup and restore qualification remain unverified/blocked. No CV Ollama artifact or real inference was qualified. Deployment configuration, OS ACLs and physical security require owner/operator evidence before release.
|
||||
Reference in New Issue
Block a user