Test the delivery as a whole—not just whichever file happens to be visible

Automatically Test SFTP Deliveries: Correlate, Claim, and Archive with Confidence

An SFTP file can be reachable and still be the wrong delivery. Its companion file may not have arrived yet, a retry may pick up the same payload again, or a valid file may remain in the input directory with no clear next state. A dependable interface check therefore needs more than content validation: it needs an explicit rule for identifying, owning, and completing a delivery.

This ZEUS Interface Quality milestone brings those related concerns together. The validator selects one unambiguous SFTP delivery, correlates its files through a shared delivery ID, claims them into a private work area, validates stable snapshots, and then archives or quarantines the complete set. The directory-selection work that was previously planned as a standalone step is included here as part of one coherent lifecycle rather than published as a separate article.

For clarity: this article deliberately combines two closely related iterations—the directory-selection work from Iteration 5 and the correlated SFTP delivery lifecycle from Iteration 6.

The broader approach is covered by the main article on automated interface quality. The published series covers JSON Schema, CSV and fixed-width files, HTTP contracts, and the stable one-file SFTP flow. This article shows how to turn the SFTP boundary into an operationally understandable delivery lifecycle.

AI-generated illustration: a protected SFTP directory routes one correlated delivery to a validation station
AI-generated illustration for tiny-tool.de: a protected SFTP directory routes one clearly correlated delivery to validation; valid data goes to the archive and invalid data goes to quarantine.

From a file match to a delivery

Directory selection was the necessary bridge from a fixed path. It is not the whole operating model, though: related files need to be recognized as one unit, and the validator needs a visible ownership boundary.

  • Explicit: the profile separates the primary file, optional companion, and shared delivery ID.
  • Complete: an orphan, partial set, or competing delivery is never guessed into a run.
  • Recoverable: an atomic rename into the claim area marks ownership.
  • Fail-closed: the delivery reaches the archive only after every selected file is valid.
  • Observable: the JSON report can expose the delivery ID, file count, and lifecycle state.

The v3 profile as an explicit contract

Version 3 builds on the bounded directory selector from version 2. filePattern identifies the primary file. companionPattern identifies its companion. Both patterns are matched against basenames only. deliveryIdPattern contains a named deliveryId group that must produce the same value for both file names.

sftp-correlated-lifecycle-profile.json · version 3
{
  "format": "sftp",
  "version": 3,
  "host": "sftp.example.test",
  "port": 22,
  "username": "quality-reader",
  "remoteDirectory": "/incoming",
  "filePattern": "^delivery-[A-Z0-9-]+[.]json$",
  "companionPattern": "^delivery-[A-Z0-9-]+[.]meta[.]json$",
  "deliveryIdPattern": "^delivery-(?[A-Z0-9-]+)(?:[.]meta)?[.]json$",
  "claimRemoteDirectory": "/claim",
  "archiveRemoteDirectory": "/archive",
  "knownHosts": "known_hosts",
  "privateKey": "id_ed25519",
  "contentFormat": "json",
  "contentProfile": "delivery.schema.json",
  "quarantineRemoteDirectory": "/quarantine",
  "stabilityChecks": 2,
  "stabilityDelaySeconds": 1,
  "maxAttempts": 2,
  "retryDelaySeconds": 1,
  "connectTimeoutSeconds": 10
}

The host-key and private-key files remain local files below the profile directory. The profile contains no passphrase; a protected key receives its passphrase only from the configured environment variable. The remote locations are intentionally separate: the producer writes to /incoming, while the validator owns /claim and /archive.

Multiple files, one delivery ID

A delivery is not complete just because one matching file exists. The v3 selector groups candidates by delivery ID. Exactly one primary and exactly one companion must form one complete delivery. This remains deterministic without hiding a business decision inside “take the newest file.”

Decisions for correlated SFTP deliveries
Remote situation Decision Next step
No complete delivery observe again while attempts remain INVALID after the final attempt
One primary and one companion with the same ID select unambiguously claim, check stability, download, and validate
Primary only or companion only incomplete delivery download nothing and reject it
Multiple complete deliveries do not guess immediate INVALID with fileCount

🟢 Correlation is a safety boundary

A name such as delivery-D-200.json does not, by itself, prove that delivery-D-200.meta.json belongs with it. The shared delivery ID makes the relationship explicit. An incomplete set does not become safe merely because a server happens to list one file first.

Claiming and safe recovery

The claim directory is the validator’s ownership boundary. Once one complete delivery has been found, every selected file is renamed remotely from /incoming to /claim. The validator downloads only after the claim succeeds. A parallel run no longer sees those files in the producer-facing directory and cannot select them again.

  1. Check the claim directory first for a delivery that was already taken.
  2. If no claim exists, inspect the input directory for one complete delivery.
  3. Rename every file in that delivery into the claim directory.
  4. If the process stops, resume the claimed delivery on the next run.

That turns “try again” into a defined recovery path. A failed claim is an operational error with exit code 2. If only part of the claim succeeds, the adapter attempts to move the files it already claimed back to the input directory; the original transfer failure remains the actionable error.

Stability, integrity, and content

Claiming does not replace validation. It protects the handoff from the producer area into the active check. The established SFTP pipeline then continues:

  1. Verify the host key and private-key access strictly.
  2. Observe type, size, and modification time as a stable snapshot for every file.
  3. Download all bytes into a temporary local directory.
  4. Compare the optional SHA-256 value with the downloaded primary file.
  5. Validate both the primary and companion file with the configured JSON, CSV, or fixed-width contract.

The shared content path is deliberately a first, bounded form of correlation: both files use the same content profile. Mixed formats or business rules such as “the total in file A must equal the total in file B” need a separate contract. Keeping that boundary visible makes the current behavior easier to trust and troubleshoot.

Archive or quarantine

A valid result is no longer just a terminal status. When claim and archive directories are configured, every checked file moves to the archive only after the complete delivery passes. When the hash or content contract fails, the entire claimed delivery moves to quarantine if a quarantine directory is enabled.

Final names include a UTC timestamp and a short collision-avoidance suffix. A later run cannot silently overwrite an earlier outcome. A failure during a final remote move is an operational error with exit code 2, not a data-contract finding.

The operational report

The shared report keeps status, valid, and findings. A v3 lifecycle may add a metadata object. It contains no payload or secrets—only the information needed to explain the state transition:

VALID · operational lifecycle report
{
  "status": "VALID",
  "valid": true,
  "findings": [],
  "metadata": {
    "deliveryId": "D-200",
    "filesValidated": 2,
    "lifecycle": "ARCHIVED",
    "archivedFiles": [
      "/archive/delivery-D-200.json.20260928180000-ab12cd34.validated",
      "/archive/delivery-D-200.meta.json.20260928180000-ef56ab78.validated"
    ]
  }
}

Older adapters and v1/v2 SFTP runs keep the compact report because empty metadata is omitted. Automation can still decide from status and findings; the lifecycle metadata adds useful context for operations, audit trails, and manual recovery.

The local proof

Readers do not need a public SFTP account to reproduce this slice. Unit tests cover correlation, incomplete deliveries, claim recovery, and remote moves through a controlled transport fake. The integration test starts an embedded Apache MINA SSHD server and runs the production JSch transport against it.

PowerShell · focused SFTP tests
mvnw.cmd -B -Dtest=SftpValidatorTest,SftpValidatorIntegrationTest test
PowerShell · complete clean build
mvnw.cmd -B clean package

🟢 Green means reproducible

The complete build finishes this milestone with 86 tests, 0 failures, 0 errors, and 2 expected skips. The skips remain the existing Windows symlink cases when local permissions are unavailable. The package phase then creates the executable JAR.

Deliberate boundaries

  • no “newest file wins” rule and no selection based on server ordering;
  • exactly one primary and, when configured, exactly one companion per correlated delivery;
  • both files currently use the same content profile;
  • no password authentication, SSH agent, or automatic OpenSSH configuration;
  • no semantic cross-file rules and no permanent directory watcher.

These boundaries are part of the contract. They keep a practical interface check from silently becoming a general-purpose SFTP orchestrator. A future step can now make an explicit decision about mixed formats and true cross-file business rules.

Conclusion: a delivery needs a lifecycle

An SFTP test becomes more dependable when it identifies the delivery as a whole instead of validating whichever file happens to be visible. Correlation answers “which files belong together?” Claim answers “who owns them now?” Archive and quarantine answer “what happened next?”

The milestone stays deliberately focused: deterministic selection, one delivery ID, safe recovery, the existing stable validation path, and a machine-readable lifecycle report. That is a practical building block for CI/CD without the ambiguity of an implicit latest-file workflow.

The Project Page tracks the overall development; the published articles 1 through 4 remain the entry points for the individual adapters.

Code and sources

Technical and editorial status: September 29, 2026. Directory selection and the new lifecycle from Iterations 5 + 6 are combined in this milestone; no standalone Iteration 5 article is published.

Transparency Notice


Transparency Notice:
Content published on tiny-tool.de is carefully researched, editorially reviewed, and updated regularly. Sources and quotations are provided in a transparent and traceable manner wherever possible. However, we do not guarantee the accuracy, completeness, or timeliness of the information provided. Errors cannot be ruled out.

Editorial Responsibility and Use of AI: Digital tools – including AI-based assistance systems – may be used to support research, structuring, language refinement, translation, code analysis, or visual production. Published content is editorially reviewed, revised, and approved by Guido Zeuner. Content selection, contextualisation, and publication remain under human responsibility. AI-generated output is not treated as an independent source. AI systems are not responsible authors or editors. More information about our use of AI in texts, images, videos, and digital personas is available on our AI Transparency page, which is currently available in German.

Audience Measurement (VG WORT / METIS): VG WORT tracking markers may be used to measure the reach of individual texts. As part of the METIS audience measurement process, a client ID may be generated and a so-called “METIS Session Cookie” may be set. The measurement is used to determine statistical access figures and as a basis for possible VG WORT remuneration. According to VG WORT, no personal usage profiles are created, and the measurement is not used for advertising or marketing tracking. More information is available in our Privacy Notice, which is currently available in German.

Please note: The content provided on this website is for general informational purposes only and does not constitute professional advice, including legal, tax, medical, technical, financial, or other specialist advice. Use of the content is at your own responsibility.

Advertising and Affiliate Links: Some posts may contain promotional references or affiliate links. These are identified accordingly. Clicking such links does not result in any additional cost to you; we may receive a small commission.

Trademark Notice: All brand names, logos, and product names are the property of their respective rights holders and are used solely for identification and descriptive purposes. No affiliation with the companies mentioned is implied unless explicitly stated.

External Links: This website contains links to external third-party websites. Although links are reviewed carefully, we assume no responsibility for the content of external websites. If we become aware of unlawful content, the respective links will be reviewed and removed where appropriate.