Skip to content

ITF-73: delimited text file steps (file.write, file.verify, file.delete) - #1

Merged
pepaooo merged 14 commits into
mainfrom
claude/fervent-bardeen-t7iomq
Nov 3, 2026
Merged

pepaooo merged 14 commits into
mainfrom
claude/fervent-bardeen-t7iomq

Conversation

@pepaooo

@pepaooo pepaooo commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Why

Many IdM projects bring identities in through the NetIQ Identity Manager Driver for Delimited Text, for example an HR export as CSV. Until now a scenario could neither drop a file for the driver nor check a file the driver writes. This PR adds three step types so that "file in, then check LDAP, the database or AD" (and the reverse) can be tested end to end.

Design: docs/specs/2026-11-03-delimited-text-file-steps-design.md · Plan: docs/plans/2026-11-03-itf-73-delimited-text-file-steps.md

What

steps:
  - file.write:
      folder: hr-in
      file: "hr-${user.userId}.csv"
      rows:
        - { employeeId: "${user.userId}", email: "${user.email}" }
      as: written
  - file.verify:
      folder: hr-out
      file: "hr-out-*.csv"
      where: { employeeId: "${user.userId}" }
      expect: { rowCount: 1, row: { status: provisioned } }
      wait: { timeout: 2m, poll: 5s }
cleanup:
  - file.delete: { folder: hr-in, file: "${written.name}" }

Folders

  • Folders are declared by name under idm.test.files.<name>, with path, an access setting (read / write / read-write, default read) and a file format.
  • A scenario never gives a path. A file name that would leave its folder is rejected: when the scenario is validated if it's written literally, and when the step runs if it's built from ${...}.
  • Messages, results, reports and the workbench name the folder by its key, never by its path.

file.write

  • Writes under a temporary name, then renames the file in one atomic step, so a polling driver never reads half a file.
  • Never overwrites an existing file. Fails if a row has a key that isn't one of the columns, so a typo is never dropped silently.

file.verify

  • Reads the newest matching file modified since the scenario started, so a file from an earlier run can't make it pass. A per-folder clock-skew allows for a share's clock.
  • Selects rows with where: and checks rowCount / row, the same way db.verify does.
  • Retried: no file yet, a file that doesn't parse yet, rows that differ. Final: configuration problems.

file.delete

  • Treats a file that is already gone as success, because the driver usually removes its input.

How

  • New module idm-connector-file: DelimitedFileConnector (Apache Commons CSV). Spring appears only in its config package.
  • Engine:
    • The three steps.
    • FileEvidence: masked and size-limited, and it always keeps the rows where: selected.
    • StepContext: gives a step the scenario's start time, through a default execute overload.
    • StepType.literalProblem: a validation hook for literal parameter values.
    • RowExpectations: db.verify's expectation checks, now shared. db.verify's messages are unchanged.
    • Messages in English and Spanish.
  • Wiring: 17 registered step types. ScenarioNeeds gains the folders read and written. A real-environment guard refuses an undeclared or missing folder, or one with too little access as declared or as the operating system allows. The guard creates nothing in the folder, because a driver may be watching it.
  • Runner and workbench:
    • Folders are lab systems (folder:<name>). Saving refuses a value the runner could not bind.
    • The connection test uses a probe file that is deleted at once.
    • The finding panel shows file evidence, without the folder's path.
    • SecretMask covers file evidence.
  • Docs: authoring guide and reference, configuration (idm.test.files), workbench guide, run guards, capability matrix, README, CLAUDE.md.

Testing

  • New tests: connector tests over real temporary folders (containment, symbolic links, atomic write, choosing the file to read, malformed files, access); FileStepsTest, including a wait through the engine while another thread writes the file; guard tests; lab and connection-test tests; idmtest run tests; Vitest tests.
  • Default-suite scenario files-delimited-round-trip.yaml: writes, waits and checks with where:, then deletes in cleanup, against a fresh folder under target/. DelimitedFileScenarioTest then checks that the folder is left empty and that no path appears in the evidence.
  • Deliberate breaks: I broke each important safeguard on purpose to confirm the tests catch it.
  • Hand-run with the built jar: an idmtest run of the round trip and a masking scenario. Neither the folder path nor the secrets.env secret appears in report.html, run.log or results.json. A workbench session confirmed the lab panel and a run.
  • ./mvnw clean install passed in every module, but on JDK 21 (-Djava.version=21 -Dmaven.compiler.release=21) and with LC_ALL=C.UTF-8, because the build container had no Java 25 JDK. Please run ./mvnw clean install on Java 25 before merging.
  • Not run: ContainerScenarios, which needs Docker. It now declares the scratch folder too.

Out of scope

  • SFTP or SMB access by the framework itself (mount the share instead), fixed-width files, and several CSV sections in one file.
  • results.json keeps step parameters unmasked by key. This is existing, documented behaviour, so a literal password in a file.write row shows up there, as userPassword in ldap.create already does. I've suggested a separate task for it.

Generated by Claude Code

claude added 14 commits November 3, 2026 00:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
New idm-connector-file module: named folders from idm.test.files with
declared access, plain-name containment (no separators, .., roots, or
symlinks resolving outside), atomic temp-then-rename writes that never
overwrite or leave a temporary file, newest-matching-file selection
since a start instant with clock skew, and Commons CSV reading/writing
with per-folder and per-call format. Failures carry a FileProblem and
never name a path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
StepType gains a default execute(params, captureRequested, StepContext)
that delegates to the existing method, so no step changes. The engine
always calls it with one context per scenario run, shared by every
step, wait attempt and cleanup entry; file.verify will use its
scenarioStartedAt to ignore files from earlier runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
FileEvidence records a file.* step's folder name (never its path), file,
operation, presence, size, header and rows. Its constructor masks
secret-named columns, cuts long values and keeps at most 50 rows within
64K characters, always keeping rows that where: selected; it is
idempotent, as SecretMask rebuilds evidence through it. The HTML report
renders it as a table with selected rows marked; JSON writes it with
kind 'file'. SecretMask masks its file name, column names and values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
file.write writes rows (literal or from fixtures) under columns or the
first row's keys, refusing unknown keys and non-scalar values.
file.verify reads the newest matching file since the scenario started,
selects rows with where: and asserts expect as db.verify does; its
expect checks and comparison move to RowExpectations, shared with
db.verify, whose messages are unchanged. file.delete treats an absent
file as success. Configuration problems are final setup failures; I/O
and unparsable files are retried. A new StepType.literalProblem hook
lets the validator reject a literal file name that leaves its folder,
and a pattern where one exact name is needed. Messages in English and
Spanish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
StepRegistry registers file.write, file.verify and file.delete (17
step types) with the DelimitedFileConnector; every caller passes it.
ScenarioNeeds collects the folders file.verify reads and file.write
and file.delete write. The real-environment guards refuse a run whose
scenarios use an undeclared or missing folder, or one whose declared
access or operating-system permissions do not allow what they do
there; they create nothing in the folder. The authoring reference
documents the three steps, as ScenarioReferenceTest requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
files-delimited-round-trip.yaml writes an HR export (one value with a
comma and quotes) into the folder scratch, waits for it and checks the
user's row with where:, checks the second row's empty field, and
deletes the file in cleanup. The test profile declares scratch as
read-write; BaseIntegrationTest and ContainerScenarios point it at a
fresh directory under target/. DelimitedFileScenarioTest runs the
scenario alone and checks that cleanup left the folder empty and that
evidence names the folder, never its path.

Also fixes FileStepsTest relying on Map.of iteration order for the
order of checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
…ry call (ITF-73)

ScenarioSupportIsolationTest calls StepRegistry.of through reflection,
so the compiler did not catch the new eighth argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
The lab panel lists folder:<name> systems (idm.test.files.<name>) the
scenarios use or the settings configure, with path (required), access
and format fields, none secret. Saving refuses a value the runner could
not bind (access, line-ending, header, clock-skew), so a typo cannot
stop the next run. The connection test probes the folder as declared,
removing its probe file, and names the folder, never its path. Delete
treats a folder like any other connection. idmtest run tests cover the
round-trip scenario against a workspace folder and the folder guard;
the editor schema lists the three steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
…(ITF-73)

Folder is a lab kind (folder:<name>) in + add, with access as a select
and the path required; an existing folder's tab gets the same select.
The finding panel shows file evidence as text: the file and folder
name, a line per row with the rows where: selected marked, and what was
left out, never the folder's path. file.* steps map to their folder.
English and Spanish texts; Vitest covers the form, + add, the evidence
text and the step mapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
Authoring guide, configuration reference (idm.test.files: every key and
default, matching temp-suffix to the driver, mounted shares only), the
authoring reference's targets and validation tables, the workbench
guide (folders in the lab panel and their connection test), the
real-environment guards in running-tests.md and the scenarios-real
README, the capability matrix, README and CLAUDE.md. Spec section 6
now says how it was built: Spring only in the connector's config
package, StepContext, literalProblem, RowExpectations, a guard that
creates nothing, folders as lab tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yRxskCGWHj1E9z8bmiVzX
@pepaooo
pepaooo merged commit 2023e84 into main Nov 3, 2026
1 check passed
@pepaooo
pepaooo deleted the claude/fervent-bardeen-t7iomq branch November 3, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants