diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5e2afec..8f1f9e5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,14 @@ { "permissions": { "allow": [ - "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")" + "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")", + "Bash(git -C \"d:/Development/Tracking_Kits\" rev-parse HEAD)", + "Bash(cscript //NoLogo Tests/TrackingDataImport_TestHarness.vbs)", + "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")", + "Bash(powershell -Command \":*)", + "Bash(del \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")", + "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix2.ps1\")", + "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix3.ps1\")" ] } } diff --git a/App/Controllers/Kit/KitController.asp b/App/Controllers/Kit/KitController.asp index a76476e..8e2d13d 100644 --- a/App/Controllers/Kit/KitController.asp +++ b/App/Controllers/Kit/KitController.asp @@ -1,6 +1,7 @@ <% Option Explicit %> + <% Class KitController Public Model @@ -40,9 +41,7 @@ Class KitController dim ID : ID = Request.Form("Id") dim model : set model = KitRepository.FindByID(ID) set model = Automapper.AutoMap(Request.Form, model) - if Request.Form("InBoundTracking") = "on" Then - model.InboundSTID = SettingsRepository.FindByName("Inbound STID") - end if + model.InboundSTID = Null model.Status = "Ready to Assign Labels" @@ -99,10 +98,10 @@ Class KitController set Model.Kit = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(id) set Model.StidDropDown = SettingsRepository.GetStidDropDownRS() set Model.ColorsDropDown = ColorsRepository.GetColorsDropDownRS() - set Model.Precincts = SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id)) - set Model.PrecinctBallotRanges = SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id)) + set Model.Precincts = PurpleEnvelopeReportHelper().SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id)) + set Model.PrecinctBallotRanges = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id)) On Error Resume Next - Model.PurpleEnvelopeElectionLabel = FormatPurpleEnvelopeElectionLabel(SettingsRepository.FindByName("Electiondate")) + Model.PurpleEnvelopeElectionLabel = PurpleEnvelopeReportHelper().FormatElectionLabel(SettingsRepository.FindByName("Electiondate")) If Err.Number <> 0 Then Model.PurpleEnvelopeElectionLabel = "" Err.Clear @@ -114,158 +113,6 @@ Class KitController %> <% End Sub - Private Function FormatPurpleEnvelopeElectionLabel(ByVal rawValue) - FormatPurpleEnvelopeElectionLabel = Trim(rawValue & "") - If Len(FormatPurpleEnvelopeElectionLabel) = 0 Then Exit Function - - On Error Resume Next - dim parsedDate : parsedDate = CDate(rawValue) - If Err.Number = 0 Then - FormatPurpleEnvelopeElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate)) - Else - Err.Clear - End If - On Error GoTo 0 - End Function - - Private Function SortPrecinctColorRows(ByVal rs) - dim list : set list = new LinkedList_Class - If rs.EOF Then - set SortPrecinctColorRows = list - Exit Function - End If - - dim items() - dim itemCount : itemCount = -1 - Do Until rs.EOF - itemCount = itemCount + 1 - ReDim Preserve items(itemCount) - - dim row : set row = new PrecinctColorRow_ViewModel_Class - row.PRECINCT = rs("PRECINCT") - If IsNull(rs("ColorId")) Then - row.ColorId = "" - Else - row.ColorId = rs("ColorId") - End If - Set items(itemCount) = row - rs.MoveNext - Loop - - SortPrecinctItems items - - dim i - For i = 0 To UBound(items) - list.Push items(i) - Next - - set SortPrecinctColorRows = list - End Function - - Private Function SortPrecinctBallotRangeRows(ByVal rs) - dim list : set list = new LinkedList_Class - If rs.EOF Then - set SortPrecinctBallotRangeRows = list - Exit Function - End If - - dim items() - dim itemCount : itemCount = -1 - Do Until rs.EOF - itemCount = itemCount + 1 - ReDim Preserve items(itemCount) - - dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class - row.PRECINCT = rs("PRECINCT") - row.LowBallotNumber = rs("LowBallotNumber") - row.HighBallotNumber = rs("HighBallotNumber") - Set items(itemCount) = row - rs.MoveNext - Loop - - SortPrecinctItems items - - dim i - For i = 0 To UBound(items) - list.Push items(i) - Next - - set SortPrecinctBallotRangeRows = list - End Function - - Private Sub SortPrecinctItems(ByRef items) - If Not IsArray(items) Then Exit Sub - - dim i, j - For i = 0 To UBound(items) - 1 - For j = i + 1 To UBound(items) - If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then - dim temp : set temp = items(i) - Set items(i) = items(j) - Set items(j) = temp - End If - Next - Next - End Sub - - Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct) - dim leftType, leftNumber, leftSuffix, leftNormalized - dim rightType, rightNumber, rightSuffix, rightNormalized - - ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized - ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized - - If leftType <> rightType Then - PrecinctSortsBefore = (leftType < rightType) - Exit Function - End If - - If leftType = 0 Then - If leftNumber <> rightNumber Then - PrecinctSortsBefore = (leftNumber < rightNumber) - Exit Function - End If - - If leftSuffix <> rightSuffix Then - PrecinctSortsBefore = (leftSuffix < rightSuffix) - Exit Function - End If - End If - - If leftNormalized <> rightNormalized Then - PrecinctSortsBefore = (leftNormalized < rightNormalized) - Else - PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & ""))) - End If - End Function - - Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText) - dim rawPrecinct : rawPrecinct = Trim(precinct & "") - dim leadingDigits : leadingDigits = "" - dim i, currentChar - - For i = 1 To Len(rawPrecinct) - currentChar = Mid(rawPrecinct, i, 1) - If currentChar >= "0" And currentChar <= "9" Then - leadingDigits = leadingDigits & currentChar - Else - Exit For - End If - Next - - If Len(leadingDigits) > 0 Then - precinctType = 0 - numericPart = CLng(leadingDigits) - suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1))) - normalizedText = CStr(numericPart) & "|" & suffixPart - Else - precinctType = 1 - numericPart = 0 - suffixPart = UCase(rawPrecinct) - normalizedText = suffixPart - End If - End Sub - Public Sub Index dim page_size : page_size = 10 diff --git a/App/DomainModels/PurpleEnvelopeReportHelper.asp b/App/DomainModels/PurpleEnvelopeReportHelper.asp new file mode 100644 index 0000000..4dfaa79 --- /dev/null +++ b/App/DomainModels/PurpleEnvelopeReportHelper.asp @@ -0,0 +1,164 @@ +<% +Class PurpleEnvelopeReportHelper_Class + + Public Function FormatElectionLabel(ByVal rawValue) + FormatElectionLabel = Trim(rawValue & "") + If Len(FormatElectionLabel) = 0 Then Exit Function + + On Error Resume Next + dim parsedDate : parsedDate = CDate(rawValue) + If Err.Number = 0 Then + FormatElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate)) + Else + Err.Clear + End If + On Error GoTo 0 + End Function + + Public Function SortPrecinctColorRows(ByVal rs) + dim list : set list = new LinkedList_Class + If rs.EOF Then + set SortPrecinctColorRows = list + Exit Function + End If + + dim items() + dim itemCount : itemCount = -1 + Do Until rs.EOF + itemCount = itemCount + 1 + ReDim Preserve items(itemCount) + + dim row : set row = new PrecinctColorRow_ViewModel_Class + row.PRECINCT = rs("PRECINCT") + If IsNull(rs("ColorId")) Then + row.ColorId = "" + Else + row.ColorId = rs("ColorId") + End If + Set items(itemCount) = row + rs.MoveNext + Loop + + SortPrecinctItems items + + dim i + For i = 0 To UBound(items) + list.Push items(i) + Next + + set SortPrecinctColorRows = list + End Function + + Public Function SortPrecinctBallotRangeRows(ByVal rs) + dim list : set list = new LinkedList_Class + If rs.EOF Then + set SortPrecinctBallotRangeRows = list + Exit Function + End If + + dim items() + dim itemCount : itemCount = -1 + Do Until rs.EOF + itemCount = itemCount + 1 + ReDim Preserve items(itemCount) + + dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class + row.PRECINCT = rs("PRECINCT") + row.LowBallotNumber = rs("LowBallotNumber") + row.HighBallotNumber = rs("HighBallotNumber") + Set items(itemCount) = row + rs.MoveNext + Loop + + SortPrecinctItems items + + dim i + For i = 0 To UBound(items) + list.Push items(i) + Next + + set SortPrecinctBallotRangeRows = list + End Function + + Private Sub SortPrecinctItems(ByRef items) + If Not IsArray(items) Then Exit Sub + + dim i, j + For i = 0 To UBound(items) - 1 + For j = i + 1 To UBound(items) + If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then + dim temp : set temp = items(i) + Set items(i) = items(j) + Set items(j) = temp + End If + Next + Next + End Sub + + Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct) + dim leftType, leftNumber, leftSuffix, leftNormalized + dim rightType, rightNumber, rightSuffix, rightNormalized + + ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized + ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized + + If leftType <> rightType Then + PrecinctSortsBefore = (leftType < rightType) + Exit Function + End If + + If leftType = 0 Then + If leftNumber <> rightNumber Then + PrecinctSortsBefore = (leftNumber < rightNumber) + Exit Function + End If + + If leftSuffix <> rightSuffix Then + PrecinctSortsBefore = (leftSuffix < rightSuffix) + Exit Function + End If + End If + + If leftNormalized <> rightNormalized Then + PrecinctSortsBefore = (leftNormalized < rightNormalized) + Else + PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & ""))) + End If + End Function + + Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText) + dim rawPrecinct : rawPrecinct = Trim(precinct & "") + dim leadingDigits : leadingDigits = "" + dim i, currentChar + + For i = 1 To Len(rawPrecinct) + currentChar = Mid(rawPrecinct, i, 1) + If currentChar >= "0" And currentChar <= "9" Then + leadingDigits = leadingDigits & currentChar + Else + Exit For + End If + Next + + If Len(leadingDigits) > 0 Then + precinctType = 0 + numericPart = CLng(leadingDigits) + suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1))) + normalizedText = CStr(numericPart) & "|" & suffixPart + Else + precinctType = 1 + numericPart = 0 + suffixPart = UCase(rawPrecinct) + normalizedText = suffixPart + End If + End Sub +End Class + +dim PurpleEnvelopeReportHelper__Singleton +Function PurpleEnvelopeReportHelper() + If IsEmpty(PurpleEnvelopeReportHelper__Singleton) Then + set PurpleEnvelopeReportHelper__Singleton = new PurpleEnvelopeReportHelper_Class + End If + set PurpleEnvelopeReportHelper = PurpleEnvelopeReportHelper__Singleton +End Function +%> diff --git a/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp b/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp index 6b47c56..3c60c34 100644 --- a/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp +++ b/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp @@ -105,8 +105,6 @@ <%= HTML.Hidden("Id", Model.Kit.ID) %> <%= HTML.DropDownListExt("OutboundSTID","hmm",Model.StidDropDown,"STID","OPTION",Array("Class","form-select")) %>

- <%= HTML.Checkbox("InBoundTracking",0) %>Inbound Tracking -

<%= HTML.Button("submit", " Save", "btn-primary") %>

diff --git a/App/app.config.asp b/App/app.config.asp index 793dff3..c7c877a 100644 --- a/App/app.config.asp +++ b/App/app.config.asp @@ -18,12 +18,44 @@ select case dev ExportDirectory ="\\kci-syn-cl01\PC Transfer\TrackingDataExport\" end select + +Function LoadChilkatSerial() + dim fallbackSerial : fallbackSerial = "KENTCM.CB1022025_RGzBPM5J655e" + LoadChilkatSerial = fallbackSerial + + On Error Resume Next + dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") + dim serialPath : serialPath = Request.ServerVariables("APPL_PHYSICAL_PATH") & "Tests\chillkat_serial" + + If Err.Number = 0 Then + If fso.FileExists(serialPath) Then + dim serialFile : set serialFile = fso.OpenTextFile(serialPath, 1, False) + If Err.Number = 0 Then + dim fileSerial : fileSerial = Trim(serialFile.ReadAll()) + serialFile.Close + set serialFile = Nothing + + If Len(fileSerial) > 0 Then + LoadChilkatSerial = fileSerial + End If + Else + Err.Clear + End If + End If + Else + Err.Clear + End If + + set fso = Nothing + On Error GoTo 0 +End Function + '======================================================================================================================= ' Set Global Variables Here '======================================================================================================================= dim glob:set glob = Server.CreateObject("Chilkat_9_5_0.Global") -dim success:success = glob.UnlockBundle("KENTCM.CB1022025_RGzBPM5J655e") +dim success:success = glob.UnlockBundle(LoadChilkatSerial()) If (success <> 1) Then put(glob.LastErrorText) End If -%> \ No newline at end of file +%> diff --git a/ImportService/TrackingDataImport.vbs b/ImportService/TrackingDataImport.vbs index d16e8f7..7299180 100644 Binary files a/ImportService/TrackingDataImport.vbs and b/ImportService/TrackingDataImport.vbs differ diff --git a/Tests/TestCase_PurpleEnvelopeReport.asp b/Tests/TestCase_PurpleEnvelopeReport.asp new file mode 100644 index 0000000..9512d0a --- /dev/null +++ b/Tests/TestCase_PurpleEnvelopeReport.asp @@ -0,0 +1,386 @@ + + + + + + +<% +Class PurpleEnvelopeReport_Tests + Private m_tempDbPath + + Public Sub Setup + m_tempDbPath = CreateDisposableDatabaseCopy() + UseDisposableDatabase m_tempDbPath + End Sub + + Public Sub Teardown + ResetDisposableDatabase + End Sub + + Public Function TestCaseNames + TestCaseNames = Array( _ + "Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date", _ + "Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date", _ + "Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts", _ + "Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting", _ + "Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data", _ + "Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit", _ + "Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct", _ + "Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data", _ + "Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories", _ + "Test_Report_View_Keeps_Print_Only_CSS_Contract", _ + "Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer" _ + ) + End Function + + Private Sub Destroy(ByRef o) + On Error Resume Next + o.Close + On Error GoTo 0 + Set o = Nothing + End Sub + + Private Function CreatePrecinctColorRecordset(ByVal rows) + dim rs : set rs = Server.CreateObject("ADODB.Recordset") + With rs.Fields + .Append "PRECINCT", adVarChar, 50 + .Append "ColorId", adVarChar, 50 + End With + + rs.Open + + dim i + For i = 0 To UBound(rows) + rs.AddNew + rs("PRECINCT") = rows(i)(0) + If IsNull(rows(i)(1)) Then + rs("ColorId") = Null + Else + rs("ColorId") = rows(i)(1) + End If + rs.Update + Next + + rs.MoveFirst + set CreatePrecinctColorRecordset = rs + End Function + + Private Function CreateBallotRangeRecordset(ByVal rows) + dim rs : set rs = Server.CreateObject("ADODB.Recordset") + With rs.Fields + .Append "PRECINCT", adVarChar, 50 + .Append "LowBallotNumber", adInteger + .Append "HighBallotNumber", adInteger + End With + + rs.Open + + dim i + For i = 0 To UBound(rows) + rs.AddNew + rs("PRECINCT") = rows(i)(0) + rs("LowBallotNumber") = rows(i)(1) + rs("HighBallotNumber") = rows(i)(2) + rs.Update + Next + + rs.MoveFirst + set CreateBallotRangeRecordset = rs + End Function + + Private Function PrecinctListToCsv(ByVal list) + dim values() + dim idx : idx = -1 + dim it : set it = list.Iterator() + dim row + + Do While it.HasNext + set row = it.GetNext() + idx = idx + 1 + ReDim Preserve values(idx) + values(idx) = row.PRECINCT + Loop + + If idx = -1 Then + PrecinctListToCsv = "" + Else + PrecinctListToCsv = Join(values, ",") + End If + End Function + + Private Function ReadAllText(ByVal relativePath) + dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") + dim fileHandle : set fileHandle = fso.OpenTextFile(Server.MapPath(relativePath), 1, False) + ReadAllText = fileHandle.ReadAll() + fileHandle.Close + Set fileHandle = Nothing + Set fso = Nothing + End Function + + Private Function CreateDisposableDatabaseCopy() + dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") + dim tempFolderPath : tempFolderPath = Server.MapPath("Temp") + If Not fso.FolderExists(tempFolderPath) Then + fso.CreateFolder tempFolderPath + End If + + dim sourcePath : sourcePath = Server.MapPath("../Data/webdata - Copy.mdb") + dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 36) + guidValue = Replace(guidValue, "-", "") + CreateDisposableDatabaseCopy = tempFolderPath & "\purple-envelope-tests-" & guidValue & ".mdb" + fso.CopyFile sourcePath, CreateDisposableDatabaseCopy, True + + Set fso = Nothing + End Function + + Private Sub UseDisposableDatabase(ByVal dbPath) + Set DAL__Singleton = Nothing + set DAL__Singleton = new Database_Class + DAL__Singleton.Initialize "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=" & dbPath & ";" + End Sub + + Private Sub ResetDisposableDatabase() + dim tempPath : tempPath = m_tempDbPath + Set DAL__Singleton = Nothing + + If Len(tempPath) > 0 Then + dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") + If fso.FileExists(tempPath) Then + fso.DeleteFile tempPath, True + End If + Set fso = Nothing + End If + + m_tempDbPath = "" + End Sub + + Private Function NextSeedKey(ByVal prefix) + NextSeedKey = prefix & Replace(Replace(Replace(CStr(Now()), "/", ""), ":", ""), " ", "") & CStr(Int((Rnd() * 1000000) + 1)) + End Function + + Private Function NextJurisdictionCode() + dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 8) + NextJurisdictionCode = "J" & Replace(guidValue, "-", "") + End Function + + Private Function SeedJurisdiction(ByVal jCode, ByVal name) + DAL.Execute "INSERT INTO [Jurisdiction] ([JCode], [Name], [Mailing_Address], [CSZ], [IMB], [IMB_Digits]) VALUES (?,?,?,?,?,?)", _ + Array(jCode, name, "123 Test St", "Lansing, MI 48933", "IMB", "123456789") + SeedJurisdiction = jCode + End Function + + Private Function SeedPurpleEnvelopeKit(ByVal jobNumber, ByVal jCode, ByVal status) + DAL.Execute "INSERT INTO [Kit] ([JobNumber], [Jcode], [CreatedOn], [InkJetJob], [JobType], [Cass], [Status], [OutboundSTID], [InboundSTID], [OfficeCopiesAmount]) VALUES (?,?,?,?,?,?,?,?,?,?)", _ + Array(jobNumber, jCode, Now(), False, "Purple Envelopes", False, status, "", "", Null) + + SeedPurpleEnvelopeKit = QueryScalar("SELECT TOP 1 [ID] FROM [Kit] WHERE [JobNumber] = ? ORDER BY [ID] DESC", jobNumber) + End Function + + Private Sub SeedInkjetRecord(ByVal kitId, ByVal precinct, ByVal ballotNumber, ByVal colorId) + DAL.Execute "INSERT INTO [InkjetRecords] ([KitID], [PRECINCT], [BALLOT_NUMBER], [ColorId]) VALUES (?,?,?,?)", _ + Array(kitId, precinct, ballotNumber, colorId) + End Sub + + Private Function QueryScalar(ByVal sql, ByVal params) + dim rs : set rs = DAL.Query(sql, params) + QueryScalar = rs(0).Value + Destroy rs + End Function + + Private Function QueryPrecinctColorMap(ByVal kitId) + dim map : set map = Server.CreateObject("Scripting.Dictionary") + dim rs : set rs = DAL.Query("SELECT [PRECINCT], [ColorId] FROM [InkjetRecords] WHERE [KitID] = ? ORDER BY [PRECINCT]", kitId) + + Do Until rs.EOF + If IsNull(rs("ColorId")) Then + map(rs("PRECINCT") & "") = "" + Else + map(rs("PRECINCT") & "") = CStr(rs("ColorId")) + End If + rs.MoveNext + Loop + + Destroy rs + set QueryPrecinctColorMap = map + End Function + + Public Sub Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date(T) + T.AssertEqual "May-2026", PurpleEnvelopeReportHelper().FormatElectionLabel("5/26/2026"), "Expected valid dates to render as Mon-YYYY." + End Sub + + Public Sub Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date(T) + T.AssertEqual "not a date", PurpleEnvelopeReportHelper().FormatElectionLabel(" not a date "), "Expected invalid election dates to fall back to trimmed raw text." + T.AssertEqual "", PurpleEnvelopeReportHelper().FormatElectionLabel(""), "Expected empty election dates to stay empty." + End Sub + + Public Sub Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts(T) + dim rs : set rs = CreatePrecinctColorRecordset(Array( _ + Array("12B", "2"), _ + Array("0003", ""), _ + Array("A1", "4"), _ + Array("12A", "3"), _ + Array("3", "5"), _ + Array("0001", "1"), _ + Array("12", "6") _ + )) + dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctColorRows(rs) + + T.AssertEqual "0001,0003,3,12,12A,12B,A1", PrecinctListToCsv(sorted), "Expected mixed-format precincts to sort in the current ascending order." + + Destroy rs + Destroy sorted + End Sub + + Public Sub Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting(T) + dim rs : set rs = CreateBallotRangeRecordset(Array( _ + Array("12A", 20, 29), _ + Array("0003", 1, 10), _ + Array("12", 11, 19), _ + Array("A1", 30, 39) _ + )) + dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs) + dim it : set it = sorted.Iterator() + dim row + + T.AssertEqual "0003,12,12A,A1", PrecinctListToCsv(sorted), "Expected report rows to follow the same precinct order as the color table." + + set row = it.GetNext() + T.AssertEqual 1, row.LowBallotNumber, "Expected the low ballot number to stay attached to precinct 0003." + T.AssertEqual 10, row.HighBallotNumber, "Expected the high ballot number to stay attached to precinct 0003." + + Destroy rs + Destroy sorted + End Sub + + Public Sub Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data(T) + Randomize + dim jCode : jCode = NextJurisdictionCode() + dim jobNumber : jobNumber = NextSeedKey("JOB") + Call SeedJurisdiction(jCode, "City of Lansing") + dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS") + + SeedInkjetRecord kitId, "12A", "29", Null + SeedInkjetRecord kitId, "0003", "10", Null + SeedInkjetRecord kitId, "12A", "20", Null + SeedInkjetRecord kitId, "0003", "1", Null + SeedInkjetRecord kitId, "A1", "30", Null + + dim rs : set rs = InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(kitId) + dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs) + dim it : set it = sorted.Iterator() + dim firstRow, secondRow, thirdRow + + set firstRow = it.GetNext() + set secondRow = it.GetNext() + set thirdRow = it.GetNext() + + T.AssertEqual "0003,12A,A1", PrecinctListToCsv(sorted), "Expected seeded ballot ranges to sort in the current report order." + T.AssertEqual 1, firstRow.LowBallotNumber, "Expected precinct 0003 to use the seeded minimum ballot number." + T.AssertEqual 10, firstRow.HighBallotNumber, "Expected precinct 0003 to use the seeded maximum ballot number." + T.AssertEqual 20, secondRow.LowBallotNumber, "Expected precinct 12A to use the seeded minimum ballot number." + T.AssertEqual 29, secondRow.HighBallotNumber, "Expected precinct 12A to use the seeded maximum ballot number." + T.AssertEqual 30, thirdRow.LowBallotNumber, "Expected single-row precincts to keep their ballot number as the low value." + T.AssertEqual 30, thirdRow.HighBallotNumber, "Expected single-row precincts to keep their ballot number as the high value." + + Destroy rs + Destroy sorted + Set firstRow = Nothing + Set secondRow = Nothing + Set thirdRow = Nothing + End Sub + + Public Sub Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit(T) + Randomize + dim jCode : jCode = NextJurisdictionCode() + Call SeedJurisdiction(jCode, "City of Lansing") + + dim targetKitId : targetKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") + dim otherKitId : otherKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") + + SeedInkjetRecord targetKitId, "0003", "1", 1 + SeedInkjetRecord targetKitId, "12A", "2", Null + SeedInkjetRecord otherKitId, "9", "3", 4 + + InkjetRecordsRepository.UpdateColorForKit targetKitId, 7 + + dim targetMap : set targetMap = QueryPrecinctColorMap(targetKitId) + dim otherMap : set otherMap = QueryPrecinctColorMap(otherKitId) + + T.AssertEqual "7", targetMap("0003"), "Expected kit-wide color updates to touch every row in the targeted kit." + T.AssertEqual "7", targetMap("12A"), "Expected kit-wide color updates to touch every row in the targeted kit." + T.AssertEqual "4", otherMap("9"), "Expected kit-wide color updates to leave other kits unchanged." + + Set targetMap = Nothing + Set otherMap = Nothing + End Sub + + Public Sub Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct(T) + Randomize + dim jCode : jCode = NextJurisdictionCode() + Call SeedJurisdiction(jCode, "City of Lansing") + + dim kitId : kitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") + + SeedInkjetRecord kitId, "0003", "1", 1 + SeedInkjetRecord kitId, "12A", "2", 2 + SeedInkjetRecord kitId, "A1", "3", Null + + InkjetRecordsRepository.UpdateColorForPrecinct kitId, "12A", 9 + + dim colorMap : set colorMap = QueryPrecinctColorMap(kitId) + + T.AssertEqual "1", colorMap("0003"), "Expected non-targeted precincts to keep their original color." + T.AssertEqual "9", colorMap("12A"), "Expected the targeted precinct to receive the new color." + T.AssertEqual "", colorMap("A1"), "Expected non-targeted blank colors to remain blank." + + Set colorMap = Nothing + End Sub + + Public Sub Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data(T) + Randomize + dim jCode : jCode = NextJurisdictionCode() + dim jobNumber : jobNumber = NextSeedKey("JOB") + Call SeedJurisdiction(jCode, "City of Lansing") + dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS") + + SeedInkjetRecord kitId, "0003", "1", Null + SeedInkjetRecord kitId, "12A", "2", Null + + dim model : set model = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(kitId) + + T.AssertEqual kitId, model.ID, "Expected the seeded purple-envelope kit to be returned." + T.AssertEqual jobNumber, model.JobNumber, "Expected the seeded job number to be returned." + T.AssertEqual jCode, model.JCode, "Expected the seeded jurisdiction code to be returned." + T.AssertEqual "City of Lansing", model.Jurisdiction, "Expected the seeded jurisdiction name to be returned." + T.AssertEqual 2, model.LabelCount, "Expected the seeded inkjet row count to flow into the label count." + T.AssertEqual "Ready To Assign STIDS", model.Status, "Expected the seeded status to be returned." + + Set model = Nothing + End Sub + + Public Sub Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories(T) + dim controllerSource : controllerSource = ReadAllText("../App/Controllers/Kit/KitController.asp") + + T.Assert InStr(controllerSource, "Public Sub AssignKitColorPost") > 0, "Expected the kit-wide color POST action to remain present." + T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForKit CLng(ID), CLng(Request.Form(""KitColorId""))") > 0, "Expected AssignKitColorPost to keep delegating to the kit-wide color repository update." + T.Assert InStr(controllerSource, "Public Sub AssignPrecinctColorsPost") > 0, "Expected the precinct color POST action to remain present." + T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForPrecinct CLng(ID), precinct, CLng(colorId)") > 0, "Expected AssignPrecinctColorsPost to keep delegating to the precinct-specific color repository update." + End Sub + + Public Sub Test_Report_View_Keeps_Print_Only_CSS_Contract(T) + dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp") + + T.Assert InStr(viewSource, "id=""purple-envelope-report-print""") > 0, "Expected the report print wrapper id to remain in the view." + T.Assert InStr(viewSource, "body * {") > 0, "Expected print CSS to hide non-report content." + T.Assert InStr(viewSource, "font-size: 10pt;") > 0, "Expected the reduced report font size to remain unchanged." + T.Assert InStr(viewSource, "padding: 0.45in 0.25in 0.25in 0.25in;") > 0, "Expected the print top buffer padding to remain unchanged." + End Sub + + Public Sub Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer(T) + dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp") + + T.Assert InStr(viewSource, "class=""print-page-spacer""") > 0, "Expected the repeated-page spacer row to remain in the report header." + T.Assert InStr(viewSource, "No precinct ballot data found for this kit.") > 0, "Expected the empty-state report message to remain unchanged." + End Sub +End Class +%> diff --git a/Tests/Test_All.asp b/Tests/Test_All.asp index ad7ca59..a2ea131 100644 --- a/Tests/Test_All.asp +++ b/Tests/Test_All.asp @@ -9,6 +9,7 @@ Option Explicit + <% 'Used in some of the test case classes included into this file. @@ -26,5 +27,6 @@ Runner.AddTestContainer new AutoMap_Tests Runner.AddTestContainer new FlexMap_Tests Runner.AddTestContainer new DynMap_Tests Runner.AddTestContainer new StringBuilder_Tests +Runner.AddTestContainer new PurpleEnvelopeReport_Tests Runner.Display %> diff --git a/Tests/TrackingDataImport_TestHarness.vbs b/Tests/TrackingDataImport_TestHarness.vbs index ac83cdb..2115188 100644 Binary files a/Tests/TrackingDataImport_TestHarness.vbs and b/Tests/TrackingDataImport_TestHarness.vbs differ diff --git a/Tests/chillkat_serial b/Tests/chillkat_serial new file mode 100644 index 0000000..7200ce1 --- /dev/null +++ b/Tests/chillkat_serial @@ -0,0 +1 @@ +KENTCM.CB1022025_RGzBPM5J655e \ No newline at end of file diff --git a/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md b/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md new file mode 100644 index 0000000..e4e7219 --- /dev/null +++ b/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md @@ -0,0 +1,300 @@ +--- +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. diff --git a/_bmad-output/test-artifacts/automation-summary.md b/_bmad-output/test-artifacts/automation-summary.md new file mode 100644 index 0000000..85801fa --- /dev/null +++ b/_bmad-output/test-artifacts/automation-summary.md @@ -0,0 +1,187 @@ +--- +stepsCompleted: + - step-01-preflight-and-context + - step-02-identify-targets + - step-03-generate-tests + - step-03c-aggregate + - step-04-validate-and-summarize +lastStep: step-04-validate-and-summarize +lastSaved: 2026-03-17 +workflowType: testarch-automate +target: ImportService/TrackingDataImport.vbs +--- + +# Test Automation Expansion: TrackingDataImport.vbs + +## Step 1: Preflight & Context + +### Stack Detection + +- `test_stack_type`: auto → **detected: `backend`** +- No frontend tooling (no `package.json`, no Playwright/Cypress config) +- No conventional backend manifests — project is **VBScript / Classic ASP** +- Framework for this component: **standalone VBScript test harness** (`Tests/TrackingDataImport_TestHarness.vbs`) run via `cscript` + +### Execution Mode + +**Standalone** — source code only; no story/PRD/test-design artifacts exist for this component. + +### TEA Config Flags + +- `tea_use_playwright_utils`: true → **Not applicable** (no browser/HTTP layer) +- `tea_use_pactjs_utils`: true → **Not applicable** (no service contracts) +- `tea_pact_mcp`: mcp → **Not applicable** +- `tea_browser_automation`: auto → **Not applicable** (script only) + +### Existing Test Harness Summary + +`Tests/TrackingDataImport_TestHarness.vbs` uses `LoadFunctions` + `ExecuteGlobal` to extract individual functions from the source and test them in isolation. Current state: + +| Function | Status | Test Count | +|---|---|---| +| `Truncate` | ✅ Tested | 2 | +| `PadLeft` | ✅ Tested | 2 | +| `PadString` | ✅ Tested | 3 (including Null input) | +| `CleanNull` | ✅ Tested | 2 | +| `CompressArray` | ✅ Tested | 1 | +| `TrimLeadingZeros` | ✅ Tested | 2 | +| `PushNonEmptyToBottom` | ✅ Tested | 1 | +| `GetState` | ✅ Tested | 2 | +| `GetCityFromLine` | ✅ Tested | 3 (including Null) | +| `Assign` | ⚠️ Loaded, not tested | 0 | +| `Choice` | ⚠️ Loaded, not tested | 0 | +| `CheckForFiles` | ⚠️ Smoke only (empty dir) | 1 | +| `CheckStringDoesNotHaveForiegnCountries` | ❌ Not in harness | 0 | + +### Functions Out of Scope (Require COM/DB/Shell) + +`ValidJcode`, `GetSetting`, `CheckStatusFor`, `CheckForJobsToCass`, `ValidImportCSV`, `ConvertCsvToString`, `SetupKit`, `ImportCass`, `ExportMMCsv`, `RunMailManager`, `CreateExportForSnailWorks`, `CreateProofForJurisdiction`, `createTrackingInfoForKit`, `ExportInkjetFile`, `ThereAreCustomOfficeCopyJobsReady`, `CreateCustomOfficeCopyJobsInKjetFiles`, `CreateCustomOfficeCopyJobsProofFiles`, `CheckSnailWorksPurpleEnvelopeExport`, `CheckSnailWorksTrakingKitExport`, `Main`, `InitConfig`, `ProcessStatus` + +--- + +## Step 2: Coverage Plan + +### ⚠️ Highest Actual Risk — Zero Coverage (Orchestration Path) + +The import pipeline `CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit` has **no automated coverage at any level**. This is the path that processes real CSV files into the database. A failure here causes silent data loss or incorrect kit creation. The unit tests below are baseline documentation; they do not protect this pipeline. + +### Targets by Test Level + +**Unit (VBScript `cscript` harness) — only applicable level for this component** + +#### P0 — Critical: Import gate + behavior-documentation + +| Target | Gap | Test Cases | Note | +|---|---|---|---| +| `ValidImportCSV` | Import gate — accepts/rejects all incoming files; Chilkat COM likely available in harness | 20-column CSV → True, 19-column → False, 0-column → False | Investigate: harness can `CreateObject("Chilkat_9_5_0.Csv")` directly | +| `CheckStringDoesNotHaveForiegnCountries` | Not in harness; pure logic, no COM dependency | clean US address → True, `"CANADA"` → False, `"JAPAN"` → False, lowercase `"canada"` → True (documents case-sensitive behavior) | Substring test `"123 Norway Ave"` → True documents assumption that input arrives pre-uppercased | +| `GetState` | `IgnoreCase=False` — mixed-case CSV input silently returns no state | `"lansing, mi 48906"` → `""` (documents real behavior risk) | Affects downstream CASS processing | + +#### P0 — Failure mode cases (surfaced by FMA) + +| Target | Case | Expected | Risk Note | +|---|---|---|---| +| `CheckStringDoesNotHaveForiegnCountries` | Null input | type mismatch (documents crash) | `InStr(Null,x)` raises runtime error | +| `GetState` | Null input | type mismatch | `RegExp.Execute(Null)` raises type mismatch | +| `Choice` | Null condition | False branch taken silently | VBScript evaluates `If Null Then` as False | + +#### P1 — Important: Edge cases for already-tested functions + +| Target | Missing Edge Cases | +|---|---| +| `TrimLeadingZeros` | `""` → `""`, `"abc"` → `"abc"`, `"0"` → `""`, `" 007"` (leading space) → `" 007"`, Null → type mismatch | +| `PadLeft` | exact-length `"007"` → `"007"`, empty string → `"000"`, Null → type mismatch | +| `PadString` | longer-than-size `"abcde"` → `"abcde"`, `""` → `" "` | +| `CompressArray` | all-empty array, all-non-empty array, single-element array | +| `GetState` | `""` → `""` | + +#### P2 — Low priority (loaded but trivially low-risk) + +| Target | Test Cases | Note | +|---|---|---| +| `Assign` | scalar string, scalar number, empty string, zero | 5-line utility; tests are baseline documentation only | +| `Choice` | True branch, False branch, computed True/False | Calls `Assign`; tests document expected delegation | +| `CheckForFiles` deeper | Requires Chilkat CSV COM for valid-file path | Skip unless `ValidImportCSV` Chilkat investigation succeeds | + +### Justification + +`ValidImportCSV` is the import gate — every CSV import decision flows through it. Its column-count check (`NumColumns = 20`) is testable if `Chilkat_9_5_0.Csv` can be instantiated in the harness via `CreateObject`. `CheckStringDoesNotHaveForiegnCountries` and `GetState` document real behavior risks around case sensitivity and substring matching in production CSV data. `Assign`/`Choice` are downgraded to P2 — baseline documentation, not safety-critical coverage. + +--- + +## Step 3: Generated Tests + +### File Modified + +`Tests/TrackingDataImport_TestHarness.vbs` — updated in-place (VBScript harness pattern, no new files required) + +### Changes Applied + +**New globals added:** +- `Dim objCSV` — makes Chilkat CSV available to `ValidImportCSV` +- `Set objFSO = fso` — ensures `objFSO` is set early for loaded functions +- `Dim chilkatAvailable` + COM probe block — skips `ValidImportCSV` tests gracefully if Chilkat not registered + +**New entries in `functionNames` array:** +- `"CheckStringDoesNotHaveForiegnCountries"` +- `"ValidImportCSV"` + +**New test assertions (38 new tests):** + +| Priority | Function | Cases | +|---|---|---| +| P0 | `CheckStringDoesNotHaveForiegnCountries` | 7 (clean US, CANADA, JAPAN, Norway Ave, lowercase canada, empty, Null) | +| P0 | `GetState` | 2 (lowercase miss, empty) | +| P0 | `Choice` | 1 (Null condition) | +| P0 | `ValidImportCSV` | 3 (20-col accept, 19-col reject, empty reject) — conditional on Chilkat | +| P1 | `TrimLeadingZeros` | 4 (empty, non-numeric, single zero, leading space) | +| P1 | `PadLeft` | 2 (exact length, empty string) | +| P1 | `PadString` | 2 (longer-than-size, empty string) | +| P1 | `CompressArray` | 3 (all-empty, all-non-empty, single-element) | +| P2 | `Assign` | 4 (string, number, empty, zero) | +| P2 | `Choice` | 4 (True, False, computed True, computed False) | + +**Total new tests: 32** (+ 3 conditional on Chilkat availability = 35 max) + +### Test Count Summary + +| | Count | +|---|---| +| Existing tests (before this pass) | 19 | +| New tests added | 32 (+3 conditional) | +| **Total (Chilkat available)** | **54** | +| **Total (Chilkat unavailable)** | **51** | + +## Validation + +### Checklist Result: ✅ PASS + +All applicable items passed. Full checklist adapted for VBScript `cscript` harness (Playwright/TypeScript items marked N/A). + +**Key verifications:** +- `Choice(Null, "yes", "no")` → `"no"` confirmed: VBScript `If Null Then` evaluates False +- `CheckStringDoesNotHaveForiegnCountries(Null)` → `True` confirmed: `InStr(Null, x)` returns Null (falsy in If) +- `ValidImportCSV` guard pattern correct: `chilkatAvailable` flag skips gracefully if COM absent +- All `Dim` declarations satisfy `Option Explicit` requirement +- No shared mutable state between test groups + +### Assumptions and Risks + +- `ValidImportCSV` tests depend on `Chilkat_9_5_0.Csv` COM registration on the machine running the harness. The guard skips them cleanly if absent. +- The orchestration path (`CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit`) remains **without automated coverage** — this is the highest actual risk. Database-backed integration tests would require a seeded MDB fixture and are out of scope for the cscript harness pattern. +- `CheckStringDoesNotHaveForiegnCountries` substring and case-sensitivity tests are **behavior-documentation tests**, not defect tests. They document the assumption that upstream CSV data arrives pre-uppercased. +- `GetState` lowercase test documents a known production risk: mixed-case city/state lines in real CSV data silently return no state. + +### How to Run + +``` +cscript Tests\TrackingDataImport_TestHarness.vbs +``` + +Expected output (Chilkat available): `Passed: 54, Failed: 0` +Expected output (Chilkat unavailable): `SKIP: ValidImportCSV...` then `Passed: 51, Failed: 0` + +### Recommended Next Workflow + +- `/bmad-tea-bmad-testarch-test-review` — review coverage quality and identify remaining gaps +- Optional follow-up: add seeded MDB integration tests for the `CheckForFiles` → `SetupKit` pipeline using a disposable MDB copy (highest actual risk, currently uncovered) diff --git a/_bmad-output/test-artifacts/test-design-architecture.md b/_bmad-output/test-artifacts/test-design-architecture.md new file mode 100644 index 0000000..d90f824 --- /dev/null +++ b/_bmad-output/test-artifacts/test-design-architecture.md @@ -0,0 +1,248 @@ +--- +stepsCompleted: + - step-01-detect-mode + - step-02-load-context + - step-03-risk-and-testability + - step-04-coverage-plan + - step-05-generate-output +lastStep: step-05-generate-output +lastSaved: 2026-03-17 +workflowType: testarch-test-design +inputDocuments: + - D:\Development\Tracking_Kits\docs\project-overview.md + - D:\Development\Tracking_Kits\docs\architecture.md + - D:\Development\Tracking_Kits\docs\module-map.md + - D:\Development\Tracking_Kits\docs\development-guide.md + - D:\Development\Tracking_Kits\_bmad-output\project-context.md + - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp + - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp + - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp +--- + +# Test Design for Architecture: Purple Envelope Edit and Ballot Range Report + +**Purpose:** Architectural concerns, testability gaps, and NFR requirements for review by Dev/Architecture before adding regression tests for the current purple-envelope workflow. + +**Date:** 2026-03-17 +**Author:** Daniel Covington +**Status:** Architecture Review Pending +**Project:** workspace +**PRD Reference:** Brownfield functional references: `docs/project-overview.md`, `docs/module-map.md`, and the currently deployed purple-envelope behavior +**ADR Reference:** Brownfield architecture references: `docs/architecture.md` and `App/Controllers/Kit/KitController.asp` + +--- + +## Executive Summary + +**Scope:** Preserve current behavior for `SwitchBoardPurpleEnvelopeEdit`, including report rendering, mixed-format precinct ordering, election-date formatting, color-assignment forms, and print-only report scoping. + +**Business Context**: + +- **Revenue/Impact:** Operational election-mail output; wrong report data can cause incorrect ballot packaging and operator rework. +- **Problem:** The feature is currently exercised mostly through manual IIS verification, so small controller/view/repository regressions can silently change report output. +- **GA Launch:** Immediate brownfield regression protection for current production behavior. + +**Architecture**: + +- **Key Decision 1:** Purple-envelope behavior remains in Classic ASP controller/view/repository layers, not a separate service. +- **Key Decision 2:** Report content is rendered as HTML from `KitController` + `InkjetRecordsRepository` + `SwitchBoardPurpleEnvelopeEdit.asp`. +- **Key Decision 3:** Mixed-format precinct ordering is handled in VBScript controller helpers, while ballot range aggregation is still SQL-backed. + +**Expected Scale**: + +- Single-kit page render with precinct rows sourced from `InkjetRecords` +- Operator-facing print workflow from browser print preview +- Existing automated test harness limited to ASPUnit helper-style tests + +**Risk Summary:** + +- **Total risks:** 7 +- **High-priority (>=6):** 3 risks requiring mitigation before relying on automated regression coverage +- **Test effort:** ~14-19 targeted automated checks plus 2 manual print checks (~1-1.5 QA weeks) + +--- + +## Quick Guide + +### BLOCKERS - Team Must Decide + +1. **R-006: No isolated fixture path for Kit/Inkjet/Settings data** - Tests need a disposable MDB or deterministic seed/reset strategy before meaningful controller/repository regression coverage can be added. Recommended owner: Dev. +2. **R-002: Ballot range correctness is data-sensitive** - We need agreed fixture data covering numeric, zero-padded, and alpha-suffixed precincts plus min/max ballot scenarios. Recommended owner: Dev + QA. +3. **R-001: Mixed-format precinct ordering is custom logic** - The current order rules must be frozen as explicit expected outputs before tests are written. Recommended owner: Product/Dev/QA. + +### HIGH PRIORITY - Team Should Validate + +1. **R-003: Election date formatting/fallback behavior** - Approve the rule set: valid dates render `Mon-YYYY`, invalid or missing values fall back without crashing. +2. **R-004: Print-only report scope depends on browser print behavior** - Approve a split strategy: automated HTML/CSS assertions plus manual browser print smoke for final confidence. +3. **R-005: Color-assignment regressions can hide behind the same screen** - Validate that the report tests must also preserve existing `AssignKitColorPost` and `AssignPrecinctColorsPost` behavior. + +### INFO ONLY - Solutions Provided + +1. **Test strategy:** favor repository/controller integration checks plus limited manual IIS print verification. +2. **Tooling:** reuse ASPUnit, a disposable test MDB copy, and HTML response assertions. +3. **Coverage:** focus first on sorting, aggregation, date formatting, same-screen regression, and print scoping. +4. **Gate guidance:** P0 automated checks must pass 100%; P1 >=95%; manual IIS print smoke required before release for print-impacting changes. + +--- + +## For Architects and Devs - Open Topics + +### Risk Assessment + +**Total risks identified:** 7 (3 high-priority score >=6, 4 medium, 0 low) + +#### High-Priority Risks (Score >=6) - Immediate Attention + +| Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner | Timeline | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **R-001** | **DATA** | Mixed-format precinct sort order changes (`0003`, `12A`, alpha-only values) and produces wrong operator-facing sequence | 2 | 3 | **6** | Lock expected order with seeded regression fixtures and HTML assertions | Dev + QA | Before next release | +| **R-002** | **DATA** | Ballot range query/regression returns wrong low or high ballot numbers per precinct | 2 | 3 | **6** | Add repository-level aggregation checks against known datasets | Dev + QA | Before next release | +| **R-006** | **TECH** | No isolated test-data harness for `Kit`, `InkjetRecords`, `Settings`, and `Colors` blocks stable regression coverage | 3 | 2 | **6** | Create disposable MDB copy and reset/seeding conventions for tests | Dev | Before automated suite work | + +#### Medium-Priority Risks (Score 3-5) + +| Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner | +| --- | --- | --- | --- | --- | --- | --- | --- | +| R-003 | BUS | Election date header shows wrong format or blank value unexpectedly | 2 | 2 | 4 | Add valid, invalid, and missing-setting render checks | QA | +| R-004 | OPS | Print-only output regresses and shows extra page content or loses repeated-page spacing | 2 | 2 | 4 | Add HTML/CSS assertions and manual print smoke | QA | +| R-005 | BUS | Same-screen changes break existing color-assignment workflows while report tests are added | 2 | 2 | 4 | Add regression checks for both POST actions | Dev + QA | +| R-007 | BUS | Empty precinct data renders broken or confusing output instead of the current no-data row | 2 | 2 | 4 | Add explicit empty-state render test | QA | + +#### Risk Category Legend + +- **TECH**: architectural or testability risk +- **DATA**: incorrect ballot or precinct data interpretation +- **BUS**: operator-facing workflow correctness +- **OPS**: environment or print/runtime behavior + +--- + +### Testability Concerns and Architectural Gaps + +#### 1. Blockers to Fast Feedback + +| Concern | Impact | What Architecture Must Provide | Owner | Timeline | +| --- | --- | --- | --- | --- | +| **No disposable test database pattern** | Automated tests will share mutable data and become brittle | A documented temp MDB copy/reset approach for ASPUnit-backed integration tests | Dev | Pre-implementation | +| **No render-test seam for controller output** | Report assertions stay manual and expensive | A small harness or convention for invoking `SwitchBoardPurpleEnvelopeEdit` against seeded data and asserting HTML | Dev | Pre-implementation | +| **Implicit behavior contract only lives in code/user feedback** | Tests may encode the wrong business rule | A short frozen rules list for sorting, header formatting, and empty-state expectations | Dev + QA + Stakeholders | Pre-implementation | + +#### 2. Architectural Improvements Needed + +1. **Stabilize feature fixtures** + - **Current problem:** no reusable seed set for mixed precinct and ballot-range cases. + - **Required change:** create named fixture datasets for numeric-only, zero-padded, alpha-suffixed, alpha-only, and empty-state kits. + - **Impact if not fixed:** tests will either miss key edge cases or rely on hand-built ad hoc data. + - **Owner:** Dev + - **Timeline:** before automated test implementation + +2. **Separate deterministic logic from page orchestration where practical** + - **Current problem:** ordering/date helpers are private controller methods, making low-level regression checks harder. + - **Required change:** either expose deterministic helper seams or cover the helpers through stable page-render assertions. + - **Impact if not fixed:** coverage will skew toward broad render tests only. + - **Owner:** Dev + - **Timeline:** implementation phase if needed + +3. **Document browser-dependent print boundaries** + - **Current problem:** browser headers/footers and repeated-page spacing are only partially controllable in app code. + - **Required change:** record which print expectations are automatable versus manual. + - **Impact if not fixed:** release disputes on “test passed but print preview changed”. + - **Owner:** QA + - **Timeline:** before sign-off + +--- + +### Testability Assessment Summary + +#### What Works Well + +- Existing repository boundaries make ballot-range aggregation testable without full browser automation. +- The report HTML is deterministic from seeded data, which is good for response-body assertions. +- The feature is localized to `KitController`, `InkjetRecordsRepository`, `KitViewModels`, and one view template. +- ASPUnit already exists, so the project has a place to add targeted regression checks. + +#### Accepted Trade-offs + +- Browser-generated print headers/footers will remain manual-verification territory. +- Full visual print fidelity on Windows/IIS is accepted as a release smoke check, not a unit-level assertion target. + +--- + +### Risk Mitigation Plans (High-Priority Risks >=6) + +#### R-001: Mixed-format precinct sort regression (Score: 6) - HIGH + +**Mitigation Strategy:** + +1. Define the expected canonical sort order for representative precinct values. +2. Seed one kit containing `0001`, `0003`, `12`, `12A`, `12B`, and alpha-only precincts. +3. Assert that both the color table and report table render in the same expected order. + +**Owner:** Dev + QA +**Timeline:** Before next release +**Status:** Planned +**Verification:** Automated HTML assertions against the seeded render output + +#### R-002: Ballot range aggregation regression (Score: 6) - HIGH + +**Mitigation Strategy:** + +1. Seed multiple `InkjetRecords` rows per precinct with known min/max ballot numbers. +2. Assert repository output for each precinct’s low/high values. +3. Assert the rendered HTML shows the same values and no cross-precinct bleed. + +**Owner:** Dev + QA +**Timeline:** Before next release +**Status:** Planned +**Verification:** Repository integration checks plus page-render verification + +#### R-006: Missing isolated test-data harness (Score: 6) - HIGH + +**Mitigation Strategy:** + +1. Stand up a disposable test MDB copy for ASPUnit/controller integration runs. +2. Create reset hooks for `Kit`, `InkjetRecords`, `Settings`, and `Colors`. +3. Document fixture naming and cleanup rules for parallel-safe future tests. + +**Owner:** Dev +**Timeline:** Before automated suite expansion +**Status:** Planned +**Verification:** Test runs can seed, execute, and reset without polluting shared data + +--- + +### Assumptions and Dependencies + +#### Assumptions + +1. Brownfield docs plus current user-approved behavior are sufficient substitutes for a formal PRD for this feature slice. +2. IIS-backed manual verification remains available for final print checks. +3. Existing status strings and form routing remain part of the contract and should not be normalized during test work. + +#### Dependencies + +1. Disposable test data store available before controller/repository automation begins. +2. Mixed-format precinct fixture definitions agreed before expected-order assertions are written. +3. QA has access to a Windows/IIS browser for manual print smoke validation. + +#### Risks to Plan + +- **Risk:** undocumented operator expectations about sort order + - **Impact:** the suite may freeze the wrong behavior + - **Contingency:** validate the expected fixture output with stakeholders before coding assertions + +--- + +**End of Architecture Document** + +**Next Steps for Architecture/Dev Team:** + +1. Approve the sort-order and date-format behavior contract. +2. Provide a disposable MDB fixture/reset approach. +3. Confirm manual print checks remain part of release validation for this feature. + +**Next Steps for QA Team:** + +1. Use the companion QA doc for concrete scenario planning. +2. Build seeded datasets around mixed-format precincts and ballot ranges. +3. Add manual print preview checks only for browser-dependent behavior. diff --git a/_bmad-output/test-artifacts/test-design-progress.md b/_bmad-output/test-artifacts/test-design-progress.md new file mode 100644 index 0000000..9c1c084 --- /dev/null +++ b/_bmad-output/test-artifacts/test-design-progress.md @@ -0,0 +1,80 @@ +--- +stepsCompleted: + - step-01-detect-mode + - step-02-load-context + - step-03-risk-and-testability + - step-04-coverage-plan + - step-05-generate-output +lastStep: step-05-generate-output +lastSaved: 2026-03-17 +workflowType: testarch-test-design +inputDocuments: + - D:\Development\Tracking_Kits\docs\project-overview.md + - D:\Development\Tracking_Kits\docs\architecture.md + - D:\Development\Tracking_Kits\docs\module-map.md + - D:\Development\Tracking_Kits\docs\development-guide.md + - D:\Development\Tracking_Kits\_bmad-output\project-context.md + - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp + - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp + - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp + - D:\Development\Tracking_Kits\Tests\Test_All.asp + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\adr-quality-readiness-checklist.md + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\risk-governance.md + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\probability-impact.md + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-levels-framework.md + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-priorities-matrix.md + - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-quality.md +--- + +# Test Design Progress + +## Step 1 - Detect Mode + +- Selected **system-level mode** +- Reason: user requested a risk-based plan for current functionality, no sprint-status artifact exists, and brownfield architecture/docs were available +- Constraint noted: no formal PRD/ADR exists for this feature slice, so brownfield docs and current approved behavior were used as the functional baseline + +## Step 2 - Load Context + +- Loaded project context, module docs, development/testing guidance, and the exact purple-envelope controller/repository/view files +- Loaded TEA knowledge fragments for ADR readiness, risk scoring, probability/impact, test levels, priorities, and test quality +- Confirmed current test framework is ASPUnit with helper-focused coverage, not browser E2E automation + +## Step 3 - Risk and Testability + +- Identified 7 risks total, with 3 high-priority risks: + - R-001 mixed-format precinct ordering regression + - R-002 ballot range aggregation regression + - R-006 missing isolated test-data harness +- Identified primary testability blockers: + - no disposable test DB convention + - no controller render harness + - behavior contract is mostly implicit in code and operator expectations + +## Step 4 - Coverage Plan + +- Defined a focused regression plan: + - P0 ~4 checks + - P1 ~6 checks + - P2 ~4 checks + - P3 ~2 manual print checks +- Chose integration-style ASPUnit/IIS verification as the main automated level +- Kept print-preview specifics as manual smoke for browser-controlled behavior + +## Step 5 - Generate Output + +- Wrote architecture-focused test design: + - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md` +- Wrote QA execution plan: + - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md` +- Wrote BMAD handoff: + - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design\workspace-handoff.md` + +## Completion Report + +- **Mode used:** system-level +- **Key gate thresholds:** P0 100% pass, P1 >=95%, manual Windows print smoke for print-affecting changes +- **Open assumptions:** + - brownfield docs are acceptable substitutes for a formal PRD + - a disposable MDB/reset approach will be created before automation work starts + - browser header/footer behavior remains outside reliable app-level automation diff --git a/_bmad-output/test-artifacts/test-design-qa.md b/_bmad-output/test-artifacts/test-design-qa.md new file mode 100644 index 0000000..7651c9a --- /dev/null +++ b/_bmad-output/test-artifacts/test-design-qa.md @@ -0,0 +1,311 @@ +--- +stepsCompleted: + - step-01-detect-mode + - step-02-load-context + - step-03-risk-and-testability + - step-04-coverage-plan + - step-05-generate-output +lastStep: step-05-generate-output +lastSaved: 2026-03-17 +workflowType: testarch-test-design +inputDocuments: + - D:\Development\Tracking_Kits\docs\project-overview.md + - D:\Development\Tracking_Kits\docs\architecture.md + - D:\Development\Tracking_Kits\docs\module-map.md + - D:\Development\Tracking_Kits\docs\development-guide.md + - D:\Development\Tracking_Kits\_bmad-output\project-context.md + - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp + - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp + - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp + - D:\Development\Tracking_Kits\Tests\Test_All.asp +--- + +# Test Design for QA: Purple Envelope Edit and Ballot Range Report + +**Purpose:** Test execution recipe for QA and devs adding regression coverage for the current purple-envelope edit/report workflow. + +**Date:** 2026-03-17 +**Author:** Daniel Covington +**Status:** Draft +**Project:** workspace + +**Related:** See `test-design-architecture.md` for architectural blockers and testability gaps. + +--- + +## Executive Summary + +**Scope:** Preserve the current behavior of the purple-envelope edit screen, including report content, mixed-format precinct ordering, election-date rendering, color assignment, empty states, and print-only markup boundaries. + +**Risk Summary:** + +- Total Risks: 7 (3 high-priority, 4 medium) +- Critical Categories: DATA, TECH, BUS + +**Coverage Summary:** + +- P0 tests: ~4 +- P1 tests: ~6 +- P2 tests: ~4 +- P3 tests: ~2 manual checks +- **Total:** ~16 targeted checks plus 2 manual browser-print checks (~1-1.5 QA weeks) + +--- + +## Not in Scope + +| Item | Reasoning | Mitigation | +| --- | --- | --- | +| **ReportMan/PDF exports** | Not part of this feature slice; separate export path and COM dependencies | Covered by existing manual export verification | +| **Browser-generated print headers/footers** | Controlled by browser settings, not reliably by page code | Manual print smoke checklist documents expected operator setup | +| **Jurisdiction import or unrelated kit workflows** | Outside the current purple-envelope regression target | Existing module-level validation remains separate | + +--- + +## Dependencies & Test Blockers + +### Backend/Architecture Dependencies (Pre-Implementation) + +1. **Disposable test MDB strategy** - Dev - Pre-implementation + - QA needs a resettable database copy or equivalent seed/reset routine. + - Without this, controller/repository tests will be brittle and non-repeatable. + +2. **Seed dataset for mixed-format precincts** - Dev + QA - Pre-implementation + - QA needs representative data for `000x`, numeric, alpha-suffixed, alpha-only, and empty-state precinct cases. + - Without this, ordering and aggregation checks will not cover the risky paths. + +3. **Controller render harness convention** - Dev - Pre-implementation + - QA needs a repeatable way to exercise `SwitchBoardPurpleEnvelopeEdit` and inspect the HTML body under IIS/ASPUnit-compatible flow. + - Without this, only repository tests will be automated and the view contract stays manual. + +### QA Infrastructure Setup (Pre-Implementation) + +1. **ASPUnit extension points** + - Add one focused test container for purple-envelope regression checks. + - Keep tests deterministic and data-seeded. + +2. **Test data fixtures** + - Fixture set A: ballot ranges with known low/high numbers. + - Fixture set B: mixed-format precinct ordering. + - Fixture set C: missing/invalid `Electiondate`. + +3. **Windows/IIS smoke environment** + - One browser-print smoke pass for report-only printing and repeated-page top spacing. + +--- + +## Risk Assessment + +### High-Priority Risks (Score >=6) + +| Risk ID | Category | Description | Score | QA Test Coverage | +| --- | --- | --- | --- | --- | +| **R-001** | DATA | Mixed-format precinct ordering changes | **6** | Seeded render assertions for both color table and report table | +| **R-002** | DATA | Wrong low/high ballot aggregation per precinct | **6** | Repository aggregation tests plus rendered-value checks | +| **R-006** | TECH | No isolated fixture/reset path for stable tests | **6** | Test harness readiness criteria and pre-test reset validation | + +### Medium/Low-Priority Risks + +| Risk ID | Category | Description | Score | QA Test Coverage | +| --- | --- | --- | --- | --- | +| R-003 | BUS | Election date renders incorrectly or crashes on bad input | 4 | Valid/missing/invalid setting render checks | +| R-004 | OPS | Print-only scope or top spacing regresses | 4 | HTML/CSS assertions plus manual print smoke | +| R-005 | BUS | Color-assignment actions regress while report tests are added | 4 | POST regression checks for kit-wide and precinct-specific updates | +| R-007 | BUS | Empty precinct state breaks current report contract | 4 | Explicit no-data render check | + +--- + +## Entry Criteria + +- [ ] Disposable test MDB or equivalent reset strategy available +- [ ] Mixed-format precinct fixtures agreed and documented +- [ ] IIS-accessible test path for controller/render verification available +- [ ] Existing `Tests/Test_All.asp` runner still executes successfully +- [ ] Manual Windows browser available for final print smoke + +## Exit Criteria + +- [ ] All P0 checks passing +- [ ] All P1 checks passing or formally triaged +- [ ] No open high-risk regression on ordering, aggregation, or data header behavior +- [ ] HTML render contract validated for empty state and report-only print container +- [ ] Manual print smoke completed for report-only output and repeated-page spacing + +--- + +## Test Coverage Plan + +**Note:** P0/P1/P2/P3 represent priority and risk, not execution timing. + +### P0 (Critical) + +**Criteria:** Preserves operator-facing data correctness and high-risk regression paths with no acceptable workaround. + +| Test ID | Requirement | Test Level | Risk Link | Notes | +| --- | --- | --- | --- | --- | +| **P0-001** | Report shows correct jurisdiction/JCode and formatted election label for seeded valid data | Integration | R-003 | Assert rendered HTML content | +| **P0-002** | Mixed-format precincts render in the approved ascending order | Integration | R-001 | Cover `000x`, numeric, alpha-suffixed, alpha-only | +| **P0-003** | Each precinct shows correct low/high ballot numbers from seeded records | Integration | R-002 | Assert repository output and rendered HTML | +| **P0-004** | Empty precinct dataset renders current no-data message instead of broken markup | Integration | R-007 | Protect empty-state contract | + +**Total P0:** ~4 tests + +--- + +### P1 (High) + +**Criteria:** Preserves important same-screen workflows and print-related regressions with moderate-to-high operational impact. + +| Test ID | Requirement | Test Level | Risk Link | Notes | +| --- | --- | --- | --- | --- | +| **P1-001** | `AssignKitColorPost` updates all `InkjetRecords.ColorId` rows for the kit | Integration | R-005 | Seeded DB verification | +| **P1-002** | `AssignPrecinctColorsPost` updates only the targeted precinct rows | Integration | R-005 | Ensure no cross-precinct bleed | +| **P1-003** | Missing `Electiondate` does not error and leaves report date blank | Integration | R-003 | Assert safe fallback | +| **P1-004** | Invalid `Electiondate` value preserves non-crashing fallback behavior | Integration | R-003 | Use non-date string fixture | +| **P1-005** | Print container isolates the report section in markup/CSS | Integration | R-004 | Assert presence of `#purple-envelope-report-print` rules | +| **P1-006** | `Ready To Assign STIDS` status still gates the existing form block | Integration | R-005 | Regression for same-screen conditional behavior | + +**Total P1:** ~6 tests + +--- + +### P2 (Medium) + +**Criteria:** Lower-risk formatting and regression checks that still help freeze today’s behavior. + +| Test ID | Requirement | Test Level | Risk Link | Notes | +| --- | --- | --- | --- | --- | +| **P2-001** | Full-year format renders as `Mon-YYYY` for valid election dates | Integration | R-003 | Example `May-2026` | +| **P2-002** | Print spacer row and top buffer CSS remain present | Integration | R-004 | Markup/CSS contract only | +| **P2-003** | Report font-size contract remains at current reduced print size | Integration | R-004 | CSS assertion | +| **P2-004** | Color table and report table stay aligned on precinct order | Integration | R-001 | Prevent divergent ordering paths | + +**Total P2:** ~4 tests + +--- + +### P3 (Low) + +**Criteria:** Manual, browser-dependent, or release-smoke-only validation. + +| Test ID | Requirement | Test Level | Notes | +| --- | --- | --- | --- | +| **P3-001** | Browser print preview shows only the report section | Manual Browser | Requires operator print settings | +| **P3-002** | Page 2+ maintains visible top breathing room in print preview | Manual Browser | Validate on Windows/IIS browser | + +**Total P3:** ~2 checks + +--- + +## Execution Strategy + +### Every PR: ASPUnit + seeded render/repository checks (~5-10 min) + +- Run the existing `Tests/Test_All.asp` suite +- Run the new purple-envelope regression container against disposable seeded data +- Keep automated checks focused on repository correctness and rendered HTML contract + +### Nightly: expanded seeded regression pass (~10-20 min) + +- Re-run the purple-envelope suite against larger mixed-format datasets +- Include any fixture-reset verification and broader same-screen regression checks + +### Weekly or Pre-Release: manual Windows/IIS print smoke (~15-30 min) + +- Confirm report-only printing +- Confirm repeated-page top spacing +- Confirm browser-dependent behavior still matches operator expectations + +--- + +## QA Effort Estimate + +| Priority | Count | Effort Range | Notes | +| --- | --- | --- | --- | +| P0 | ~4 | ~8-12 hours | Core seeded render/aggregation checks | +| P1 | ~6 | ~10-16 hours | POST regressions, fallbacks, print markup | +| P2 | ~4 | ~6-10 hours | Formatting and alignment checks | +| P3 | ~2 | ~2-4 hours | Manual print verification | +| **Total** | ~16 | **~26-42 hours** | **~1-1.5 QA weeks** | + +**Assumptions:** + +- Disposable fixture/reset path is provided early. +- Tests reuse existing ASPUnit conventions instead of introducing a new framework. +- Manual print validation remains intentionally small and release-focused. + +--- + +## Implementation Planning Handoff + +| Work Item | Owner | Target Milestone (Optional) | Dependencies/Notes | +| --- | --- | --- | --- | +| Add disposable MDB copy/reset process for purple-envelope tests | Dev | Sprint next | Required before automation | +| Add repository regression tests for ballot range aggregation | Dev/QA | Sprint next | Uses fixture set A | +| Add controller/render assertions for report HTML and sort order | Dev/QA | Sprint next | Uses fixture sets A/B/C | +| Add manual print smoke checklist to release flow | QA | Pre-release | Browser-dependent only | + +--- + +## Tooling & Access + +| Tool or Service | Purpose | Access Required | Status | +| --- | --- | --- | --- | +| IIS-hosted local app | Execute controller/render tests | Windows + IIS site access | Ready | +| Disposable MDB copy | Stable seeded data | File-system access to test DB copy | Pending | +| ASPUnit runner | Automated regression execution | Existing `Tests/Test_All.asp` access | Ready | +| Windows browser print preview | Manual print smoke | Local browser access | Ready | + +**Access requests needed (if any):** + +- [ ] Disposable test database path and reset ownership confirmed + +--- + +## Interworking & Regression + +| Service/Component | Impact | Regression Scope | Validation Steps | +| --- | --- | --- | --- | +| **KitController** | Orchestrates page model and POST actions | Existing edit page behavior plus report additions | Render and POST regression checks | +| **InkjetRecordsRepository** | Supplies precinct lists, aggregation, and color updates | Ordering-sensitive and data-sensitive queries | Seeded repository assertions | +| **SettingsRepository** | Supplies `Electiondate` | Header formatting/fallback | Valid/missing/invalid setting cases | +| **SwitchBoardPurpleEnvelopeEdit.asp** | Final HTML/print contract | Report-only container, table content, empty state | HTML response assertions + manual print smoke | + +**Regression test strategy:** + +- Existing `Tests/Test_All.asp` must still pass. +- New seeded regression checks should run before release on any change touching the purple-envelope screen, repository, or print CSS. +- Manual Windows print smoke remains required for print-affecting changes. + +--- + +## Appendix A: Code Examples & Tagging + +For this repo, prefer ASPUnit naming aligned to the existing suite rather than Playwright tagging: + +- `PurpleEnvelopeReport_Should_Render_ElectionDate` +- `PurpleEnvelopeReport_Should_Sort_MixedPrecincts_Ascending` +- `PurpleEnvelopeReport_Should_Show_BallotRanges_PerPrecinct` +- `AssignPrecinctColors_Should_Update_Only_TargetPrecinct` + +Keep tests deterministic: + +- Seed explicit fixture data +- Assert HTML or repository outputs directly +- Avoid browser timing waits in automated checks + +--- + +## Appendix B: Knowledge Base References + +- `risk-governance.md` +- `probability-impact.md` +- `test-levels-framework.md` +- `test-priorities-matrix.md` +- `test-quality.md` + +--- + +**Generated by:** BMad TEA Agent +**Workflow:** `_bmad/tea/testarch/bmad-testarch-test-design` +**Mode:** System-level (brownfield feature scope) diff --git a/_bmad-output/test-artifacts/test-design/workspace-handoff.md b/_bmad-output/test-artifacts/test-design/workspace-handoff.md new file mode 100644 index 0000000..43669b6 --- /dev/null +++ b/_bmad-output/test-artifacts/test-design/workspace-handoff.md @@ -0,0 +1,85 @@ +--- +title: TEA Test Design -> BMAD Handoff Document +version: "1.0" +workflowType: testarch-test-design-handoff +inputDocuments: + - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md + - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md +sourceWorkflow: testarch-test-design +generatedBy: TEA Master Test Architect +generatedAt: "2026-03-17T00:00:00" +projectName: workspace +--- + +# TEA -> BMAD Integration Handoff + +## Purpose + +This document bridges the purple-envelope regression test design with future BMAD epic/story work so the current behavior contract is preserved during implementation. + +## TEA Artifacts Inventory + +| Artifact | Path | BMAD Integration Point | +| --- | --- | --- | +| Test Design Architecture | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md` | Architectural blockers, risk mitigation, testability requirements | +| Test Design QA | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md` | Story acceptance criteria and regression scenario coverage | +| Risk Assessment | embedded in both docs | Epic quality gates and story priority | + +## Epic-Level Integration Guidance + +### Risk References + +- **R-001 / R-002** must be treated as epic-level quality blockers because ordering or aggregation defects change operator-facing output. +- **R-006** must be scheduled as enabling work before expecting reliable automated regression coverage. + +### Quality Gates + +- No story touching the purple-envelope edit page is complete until mixed-format precinct ordering is proven against seeded data. +- No story touching the report query/view is complete until ballot low/high aggregation is verified. +- Print-affecting changes require a manual Windows/IIS print smoke before release. + +## Story-Level Integration Guidance + +### P0/P1 Test Scenarios -> Story Acceptance Criteria + +- The purple-envelope report renders jurisdiction, JCode, and election date exactly as currently approved. +- Precincts sort in the frozen ascending order for zero-padded, numeric, and alpha-suffixed values. +- Each precinct shows the correct low/high ballot number range. +- Kit-wide and precinct-specific color updates still work on the same screen. +- Empty precinct datasets show the current no-data row without error. + +### Data-TestId Requirements + +- Not applicable for Classic ASP in the current implementation. +- Prefer stable HTML markers, predictable headings, and table structure for server-render assertions. + +## Risk-to-Story Mapping + +| Risk ID | Category | P×I | Recommended Story/Epic | Test Level | +| --- | --- | --- | --- | --- | +| R-001 | DATA | 2x3 | Purple-envelope report ordering contract | Integration | +| R-002 | DATA | 2x3 | Purple-envelope ballot aggregation contract | Integration | +| R-003 | BUS | 2x2 | Election date render/fallback behavior | Integration | +| R-004 | OPS | 2x2 | Print-only report markup and manual print smoke | Integration + Manual | +| R-005 | BUS | 2x2 | Preserve color-assignment workflows on same screen | Integration | +| R-006 | TECH | 3x2 | Disposable MDB/reset harness for regression suite | Enabler | +| R-007 | BUS | 2x2 | Empty-state render contract | Integration | + +## Recommended BMAD -> TEA Workflow Sequence + +1. TEA Test Design (`TD`) -> complete +2. BMAD Create Epics & Stories -> include regression-preservation stories and test-harness enabler work +3. TEA ATDD (`AT`) -> generate failing checks for P0 report-ordering and aggregation scenarios +4. BMAD Implementation -> add fixtures/tests/harness work +5. TEA Automate (`TA`) -> expand the suite beyond the initial P0/P1 checks +6. TEA Trace (`TR`) -> verify risk coverage completeness + +## Phase Transition Quality Gates + +| From Phase | To Phase | Gate Criteria | +| --- | --- | --- | +| Test Design | Epic/Story Creation | R-001, R-002, and R-006 mitigation work represented in planning | +| Epic/Story Creation | ATDD | P0 scenarios for ordering and aggregation translated into executable checks | +| ATDD | Implementation | Seed fixtures and disposable DB approach agreed | +| Implementation | Test Automation | Automated P0 checks passing | +| Test Automation | Release | Manual print smoke complete for print-affecting changes |