---
title: 'ExportInkjetFile Integration Test'
slug: 'exportinkjetfile-integration-test'
created: '2026-03-17'
status: 'Implementation Complete'
stepsCompleted: [1, 2, 3, 4]
tech_stack: ['VBScript', 'ADODB', 'Chilkat_9_5_0.Csv', 'Access MDB/Jet']
files_to_modify: ['Tests/TrackingDataImport_TestHarness.vbs']
code_patterns: ['LoadFunctions+ExecuteGlobal isolation', 'ADODB connection to MDB', 'Chilkat CSV read-back']
test_patterns: ['Integration test with real MDB fixture', 'Assert file output + DB side effect']
---

# Tech-Spec: ExportInkjetFile Integration Test

**Created:** 2026-03-17

## Overview

### Problem Statement

`ExportInkjetFile(KitID)` produces the inkjet operator CSV used on press day. It has zero automated coverage. The function joins InkjetRecords + KitLabels + Colors from an Access MDB, builds a Chilkat CSV with 22 columns, writes it to disk, then marks Kit.Status='Done'. A regression here causes silent bad output with no test catching it.

### Solution

Add an ADODB integration test directly to `Tests/TrackingDataImport_TestHarness.vbs`. The harness opens `Data/webdata - Copy.mdb` via ADODB, calls `ExportInkjetFile` on a real KitID, and asserts CSV structure (file exists, 22 columns, correct headers) plus ~10 row-level field values and the DB side-effect (Kit.Status='Done', InkJetJob=1).

### Scope

**In Scope:**
- Set up integration test globals in harness: `oConn`, `ConnectionString` (pointing to `Data/webdata - Copy.mdb`), `ExportDirectory` (temp path under `Tests/`)
- Load `GetSetting` and `ExportInkjetFile` into the `functionNames` array
- Query MDB to discover a real KitID that has InkjetRecords
- Call `ExportInkjetFile(KitID)`
- Assert: CSV file exists at expected path
- Assert: 22 column headers correct (spot-check cols 0, 5, 8, 11, 19, 20, 21)
- Assert: CSV row count = InkjetRecords count for that KitID
- Assert: First 10 rows — "Ballot Number" has leading zeros stripped, "Matching Code" starts with JCode
- Assert: Kit.Status = 'Done' and Kit.InkJetJob = 1 after call
- Cleanup: delete temp export folder

**Out of Scope:**
- Pure logic extraction from `ExportInkjetFile` (not viable — all field assembly is inline with recordset reads; refactoring would be required)
- Seeding fixture data (real kit + inkjet records already exist in `Data/webdata - Copy.mdb`)
- Office copies rows (separate conditional path; can be added later if a Kit with OfficeCopiesAmount > 0 is present)
- Full row-by-row validation (10-record spot check is sufficient)
- Restoring Kit.Status after test (MDB copy is disposable; original `webdata.mdb` is untouched)

---

## Context for Development

### Codebase Patterns

- **Harness pattern**: `LoadFunctions(filePath, functionNames)` extracts named functions from source via text parsing; `ExecuteGlobal` makes them available in harness global scope. Global vars set before calling functions are visible inside loaded functions.
- **`LoadFunctions` is called at line 34**, before any test code. Adding `"GetSetting"` and `"ExportInkjetFile"` to the `functionNames` array (lines 16–32) is sufficient — they'll be extracted and available globally.
- **`Set objFSO = fso` at line 36** — already set. `ExportInkjetFile` uses `objFSO` internally; this is already satisfied.
- **`chilkatAvailable` declared at line 39** — already exists. New `integrationDbAvailable` follows the same declare-in-preamble, set-in-init-block pattern.
- **Assertion API**: The harness has **no `Assert` sub** — only `AssertEqual(actual, expected, label)` and `AssertArrayEqual`. All integration test assertions must use `AssertEqual condition, True, "label"` form.
- **DB dependency chain**: `ExportInkjetFile` calls `oConn.Open(ConnectionString)` if `oConn.State = 0`. It manages its own connection lifecycle. The harness must set `oConn` (via `Set oConn = CreateObject("ADODB.Connection")`) and `ConnectionString` as globals before calling.
- **`ExportDirectory` global**: Used by `ExportInkjetFile` as-is (no trailing slash added internally). Must include trailing `\` in the harness assignment.
- **Chilkat CSV write-back verification**: `ExportInkjetFile` creates its own internal `Chilkat_9_5_0.Csv` object. Chilkat unlock is process-wide — already done in harness init block. Read-back uses a separate object.

### Files to Reference

| File | Purpose |
| ---- | ------- |
| `Tests/TrackingDataImport_TestHarness.vbs` | Target file — 3 insertion points: globals (after line 15), `functionNames` array (after line 31), init block (after line 69), test block (before line 206) |
| `ImportService/TrackingDataImport.vbs` | Source — `ExportInkjetFile` at line 251 (Function), `GetSetting` also in same file |
| `Data/webdata - Copy.mdb` | MDB fixture — real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings |

### Technical Decisions

- **Use `Data/webdata - Copy.mdb` directly**: It's already a copy and contains real data. No need to seed a new fixture. The UPDATE side effect (Kit.Status='Done') is acceptable since this MDB is a disposable copy.
- **Discover KitID at runtime via JOIN**: Query `SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir INNER JOIN Kit k ON ir.KitID = k.ID) INNER JOIN Jurisdiction j ON k.JCode = j.JCode) INNER JOIN Contacts c ON k.JCode = c.JURISCODE` to ensure discovered KitID has all required related records. Avoid orphan-crash on `JurisdictionRs` or `ContactRs` inside the function.
- **ExportDirectory as temp path**: Set to `fso.BuildPath(scriptDir, "export-test-output")`. **Delete before call** (not just after) to ensure no stale CSV from a prior failed run contaminates assertions.
- **ADODB driver probe**: Try `Microsoft.ACE.OLEDB.12.0` first; fall back to `Microsoft.Jet.OLEDB.4.0`. If both fail, skip with message. Guard with `fso.FileExists(integrationMdbPath)` before attempting open.
- **Load both `GetSetting` and `ExportInkjetFile`** in `functionNames` array — `GetSetting` is a DB-backed dependency called internally by `ExportInkjetFile`.
- **Chilkat read-back**: Capture `verifyCsv.LoadFile(csvPath)` return value and assert it before proceeding to column/row assertions — prevents vacuously-passing assertions on a failed load.
- **Cleanup is error-suppressed**: Wrap `fso.DeleteFolder` in `On Error Resume Next` / `On Error GoTo 0` — AV scanners may briefly hold a handle on the new CSV; a cleanup failure should not fail the test.

---

## Implementation Plan

### Tasks

- [x] **Task 1: Add integration test globals and ADODB probe block**
  - File: `Tests/TrackingDataImport_TestHarness.vbs`
  - Action: Insert 7 `Dim` declarations after line 15; insert ADODB probe block after line 69
  - Notes: Sets `integrationDbAvailable`/`integrationDbSkipReason` — same guard pattern as `chilkatAvailable`

**Insertion point: after line 15** (`Dim DataDirectory`). Add:

```vbscript
Dim oConn
Dim ConnectionString
Dim ExportDirectory
Dim integrationMdbPath
Dim integrationExportDir
Dim integrationDbAvailable
Dim integrationDbSkipReason
```

**Insertion point: after line 69** (`On Error GoTo 0` at end of Chilkat init block). Add ADODB probe block:

```vbscript
integrationMdbPath = fso.BuildPath(scriptDir, "..\Data\webdata - Copy.mdb")
integrationExportDir = fso.BuildPath(scriptDir, "export-test-output")
integrationDbAvailable = False
integrationDbSkipReason = ""
If Not fso.FileExists(integrationMdbPath) Then
    integrationDbSkipReason = "MDB fixture not found: " & integrationMdbPath
Else
    Set oConn = CreateObject("ADODB.Connection")
    On Error Resume Next
    oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & integrationMdbPath & ";"
    If Err.Number <> 0 Then
        Err.Clear
        oConn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & integrationMdbPath & ";"
    End If
    If Err.Number <> 0 Then
        integrationDbSkipReason = "No ADODB provider available (ACE and Jet both failed): " & Err.Description
        Err.Clear
    Else
        integrationDbAvailable = True
        ConnectionString = oConn.ConnectionString
        ExportDirectory = integrationExportDir & "\"
    End If
    On Error GoTo 0
    If oConn.State = 1 Then oConn.Close
End If
```

- [x] **Task 2: Add `GetSetting` and `ExportInkjetFile` to `functionNames` array**
  - File: `Tests/TrackingDataImport_TestHarness.vbs`
  - Action: Extend array at lines 30–31 to include two new entries
  - Notes: Must be done before `LoadFunctions` call at line 34; order within array does not matter

**Insertion point: lines 30–31**. Change:
```vbscript
    "CheckStringDoesNotHaveForiegnCountries", _
    "ValidImportCSV" _
```
To:
```vbscript
    "CheckStringDoesNotHaveForiegnCountries", _
    "ValidImportCSV", _
    "GetSetting", _
    "ExportInkjetFile" _
```

- [x] **Task 3: Add ExportInkjetFile integration test block**
  - File: `Tests/TrackingDataImport_TestHarness.vbs`
  - Action: Insert full integration test block before line 206 (`WScript.Echo ""`)
  - Notes: Guarded by `chilkatAvailable` AND `integrationDbAvailable`; uses `AssertEqual` throughout (no `Assert` sub exists); pre-call dir cleanup, post-call DB side-effect check, error-suppressed cleanup

**Insertion point: before line 206** (`WScript.Echo ""`). Add:

```vbscript
' === ExportInkjetFile Integration Test ===
If Not chilkatAvailable Then
    WScript.Echo "SKIP: ExportInkjetFile integration test (Chilkat unavailable)"
ElseIf Not integrationDbAvailable Then
    WScript.Echo "SKIP: ExportInkjetFile integration test (" & integrationDbSkipReason & ")"
Else
    oConn.Open ConnectionString

    Dim kitDiscoverRs
    Set kitDiscoverRs = oConn.Execute( _
        "SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir " & _
        "INNER JOIN Kit k ON ir.KitID = k.ID) " & _
        "INNER JOIN Jurisdiction j ON k.JCode = j.JCode) " & _
        "INNER JOIN Contacts c ON k.JCode = c.JURISCODE;")
    If kitDiscoverRs.EOF Then
        WScript.Echo "SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB"
        oConn.Close
    Else
        Dim testKitID : testKitID = kitDiscoverRs("KitID").Value

        Dim countRs
        Set countRs = oConn.Execute("SELECT COUNT(*) AS N FROM InkjetRecords WHERE KitID=" & testKitID & ";")
        Dim expectedRows : expectedRows = countRs("N").Value

        Dim kitRsCheck
        Set kitRsCheck = oConn.Execute("SELECT JCode FROM Kit WHERE ID=" & testKitID & ";")
        Dim testJCode : testJCode = kitRsCheck("JCode").Value

        oConn.Close

        ' Delete stale output before call, then recreate
        On Error Resume Next
        If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True)
        On Error GoTo 0
        fso.CreateFolder integrationExportDir

        ExportInkjetFile testKitID

        Dim exportSubfolders : Set exportSubfolders = fso.GetFolder(integrationExportDir).SubFolders
        Dim csvFound : csvFound = False
        Dim csvPath : csvPath = ""
        Dim sf
        For Each sf In exportSubfolders
            Dim csvFiles : Set csvFiles = sf.Files
            Dim f
            For Each f In csvFiles
                If LCase(fso.GetExtensionName(f.Name)) = "csv" Then
                    csvFound = True
                    csvPath = f.Path
                End If
            Next
        Next

        AssertEqual csvFound, True, "[INT] ExportInkjetFile: CSV file created"

        If csvFound Then
            Dim verifyCsv : Set verifyCsv = CreateObject("Chilkat_9_5_0.Csv")
            verifyCsv.HasColumnNames = 1
            Dim csvLoaded : csvLoaded = verifyCsv.LoadFile(csvPath)
            AssertEqual csvLoaded, True, "[INT] ExportInkjetFile: CSV loaded by Chilkat"

            If csvLoaded Then
                AssertEqual verifyCsv.NumColumns, 22, "[INT] ExportInkjetFile: 22 columns"
                AssertEqual verifyCsv.ColumnName(0), "Full Name", "[INT] ExportInkjetFile: col 0 = Full Name"
                AssertEqual verifyCsv.ColumnName(5), "IM barcode Characters", "[INT] ExportInkjetFile: col 5 = IM barcode Characters"
                AssertEqual verifyCsv.ColumnName(8), "Ballot Number", "[INT] ExportInkjetFile: col 8 = Ballot Number"
                AssertEqual verifyCsv.ColumnName(11), "Combined  Pct_Ballot Num", "[INT] ExportInkjetFile: col 11 = Combined  Pct_Ballot Num"
                AssertEqual verifyCsv.ColumnName(19), "Matching Code", "[INT] ExportInkjetFile: col 19 = Matching Code"
                AssertEqual verifyCsv.ColumnName(20), "ColorFilepath", "[INT] ExportInkjetFile: col 20 = ColorFilepath"
                AssertEqual verifyCsv.ColumnName(21), "ColorName", "[INT] ExportInkjetFile: col 21 = ColorName"
                AssertEqual verifyCsv.NumRows, expectedRows, "[INT] ExportInkjetFile: row count matches InkjetRecords"

                Dim checkRows : checkRows = 10
                If verifyCsv.NumRows < 10 Then checkRows = verifyCsv.NumRows
                Dim r
                For r = 0 To checkRows - 1
                    Dim ballotNum : ballotNum = verifyCsv.GetCell(r, 8)
                    AssertEqual (Left(ballotNum, 1) <> "0" Or ballotNum = ""), True, "[INT] ExportInkjetFile: row " & r & " Ballot Number no leading zeros"
                    Dim matchCode : matchCode = verifyCsv.GetCell(r, 19)
                    AssertEqual Left(matchCode, Len(testJCode)), testJCode, "[INT] ExportInkjetFile: row " & r & " Matching Code starts with JCode"
                Next
            End If

            Set verifyCsv = Nothing
        End If

        oConn.Open ConnectionString
        Dim sideEffectRs
        Set sideEffectRs = oConn.Execute("SELECT Status, InkJetJob FROM Kit WHERE ID=" & testKitID & ";")
        AssertEqual sideEffectRs("Status").Value, "Done", "[INT] ExportInkjetFile: Kit.Status = Done"
        AssertEqual sideEffectRs("InkJetJob").Value, 1, "[INT] ExportInkjetFile: Kit.InkJetJob = 1"
        oConn.Close

        On Error Resume Next
        If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True)
        On Error GoTo 0
    End If
End If
```

- [x] **Task 4: Verify `Option Explicit` compliance**
  - File: `Tests/TrackingDataImport_TestHarness.vbs`
  - Action: Review all new variable names introduced in Tasks 1–3 and confirm each has a `Dim` declaration
  - Notes: New inline `Dim`s in Task 3: `kitDiscoverRs`, `testKitID`, `countRs`, `expectedRows`, `kitRsCheck`, `testJCode`, `exportSubfolders`, `csvFound`, `csvPath`, `sf`, `csvFiles`, `f`, `verifyCsv`, `csvLoaded`, `checkRows`, `r`, `ballotNum`, `matchCode`, `sideEffectRs` — all must be present

### Acceptance Criteria

- [ ] **AC 1:** Given Chilkat is available and `Data/webdata - Copy.mdb` contains a Kit with InkjetRecords, Jurisdiction, and Contact, when the harness calls `ExportInkjetFile(testKitID)`, then a CSV file is created under `Tests/export-test-output/<JobNumber>-<Name>/`, it has exactly 22 columns with correct header names at indices 0/5/8/11/19/20/21, row count matches the InkjetRecords count, the first 10 rows have no leading zeros in "Ballot Number", the first 10 rows have "Matching Code" starting with JCode, and Kit.Status='Done' + Kit.InkJetJob=1 in the MDB.

- [ ] **AC 2:** Given Chilkat is unavailable, when the harness reaches the ExportInkjetFile block, then it prints `SKIP: ExportInkjetFile integration test (Chilkat unavailable)` and no test failures are recorded.

- [ ] **AC 3:** Given `Data/webdata - Copy.mdb` is absent or neither ACE nor Jet ADODB driver is available, when the ADODB probe block runs during harness init, then `integrationDbAvailable` is False, the test block prints a SKIP message with the reason, and no test failures are recorded.

- [ ] **AC 4:** Given the MDB contains no Kit with all required related records (InkjetRecords + Jurisdiction + Contacts), when the discovery JOIN query runs, then it prints `SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB` and no test failures are recorded.

- [ ] **AC 5:** Given a prior test run left `Tests/export-test-output/` behind (simulating a failed cleanup), when the integration test runs, then the stale folder is deleted before `ExportInkjetFile` is called and the test proceeds without contaminated assertions.

---

## Additional Context

### Dependencies

- `Chilkat_9_5_0.Csv` COM — must be registered and unlocked (guarded by `chilkatAvailable`)
- `Microsoft.ACE.OLEDB.12.0` or `Microsoft.Jet.OLEDB.4.0` — must be available for ADODB to open the MDB
- `Data/webdata - Copy.mdb` — must exist with real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings(ElectionDate)
- `ADODB.Connection` — available on any Windows machine with Office/Access drivers

### Testing Strategy

Integration test only — no unit-level testing is viable for this function without significant refactoring. The test confirms the full output pipeline from DB read → CSV write → DB update.

### Notes

- The `ExportInkjetFile` function uses `objFSO` (not `fso`). The line `Set objFSO = fso` was added in a prior workflow pass and is already present in the harness.
- Column index 11 has a double-space in its name: `"Combined  Pct_Ballot Num"` — this is as-written in the source (line 296).
- The Chilkat CSV column indices: source sets cols 0–21 (22 columns total). The `ColumnName(n)` method on the read-back CSV object uses 0-based index.
- **ADODB driver probe**: Init block tries ACE first (`Microsoft.ACE.OLEDB.12.0`), falls back to Jet (`Microsoft.Jet.OLEDB.4.0`). Bitness matters — 32-bit cscript requires 32-bit drivers. Sets `integrationDbAvailable = True/False` and `integrationDbSkipReason`.
- **oConn lifecycle**: The probe block opens and immediately closes `oConn` to validate the connection string. `ExportInkjetFile` internally calls `oConn.Open(ConnectionString)` if State=0 — so close it before calling the function. Reopen after for the side-effect assertion.
- **KitID discovery JOIN**: Uses 4-table JOIN (InkjetRecords + Kit + Jurisdiction + Contacts) to guarantee no orphan-crash inside `ExportInkjetFile` when it accesses `JurisdictionRs("Name")` or `ContactRs("Title")`.
- **Pre-call cleanup**: `integrationExportDir` is deleted before calling `ExportInkjetFile` (not just after) to prevent stale CSVs from prior failed runs contaminating assertions.
