--- 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/-/`, 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.