Files
isphere-ai-bridge/internal/uiaselector/report_test.go
2026-07-09 17:14:32 +08:00

192 lines
6.1 KiB
Go

package uiaselector
import (
"strings"
"testing"
"time"
)
func fixedReportTime() time.Time {
return time.Date(2026, 7, 9, 0, 0, 0, 0, time.UTC)
}
func loadN14Fixture(t *testing.T) *Node {
t.Helper()
root, err := LoadDumpFile("testdata/n12r-2026-07-09-uia-redacted.json")
if err != nil {
t.Fatalf("LoadDumpFile: %v", err)
}
return root
}
func TestRunCatalogReportMatchesAllN13Selectors(t *testing.T) {
report := RunCatalogReport(
"testdata/n12r-2026-07-09-uia-redacted.json",
loadN14Fixture(t),
DefaultCatalog(),
fixedReportTime(),
)
if !report.OK {
t.Fatalf("report OK=false: %#v", report.Error)
}
if report.SourceDump != "testdata/n12r-2026-07-09-uia-redacted.json" {
t.Fatalf("source_dump = %q", report.SourceDump)
}
if report.GeneratedAtUTC != "2026-07-09T00:00:00Z" {
t.Fatalf("generated_at_utc = %q", report.GeneratedAtUTC)
}
if report.CatalogSize != 10 {
t.Fatalf("catalog_size = %d, want 10", report.CatalogSize)
}
if report.MatchedCount != 10 {
t.Fatalf("matched_count = %d, want 10", report.MatchedCount)
}
if report.UnmatchedCount != 0 {
t.Fatalf("unmatched_count = %d, want 0", report.UnmatchedCount)
}
if report.AmbiguousCount != 0 {
t.Fatalf("ambiguous_count = %d, want 0", report.AmbiguousCount)
}
if got := StrictExitCode(report); got != StrictExitOK {
t.Fatalf("StrictExitCode = %d, want %d", got, StrictExitOK)
}
}
func TestStrictModeClassifiesFailures(t *testing.T) {
catalog := []Selector{
{
ID: "missing_panel",
Description: "missing structural test selector",
AllowedUse: AllowedUseReadOnlyLocate,
Required: Criteria{AutomationID: "does_not_exist", ControlType: "Pane", FrameworkID: "WinForm"},
StabilityScore: 80,
Evidence: Evidence{CaptureID: "unit-test", UIAPath: "root/missing"},
},
}
report := RunCatalogReport("fixture.json", loadN14Fixture(t), catalog, fixedReportTime())
if !report.OK {
t.Fatalf("report OK=false: %#v", report.Error)
}
if report.UnmatchedCount != 1 {
t.Fatalf("unmatched_count = %d, want 1", report.UnmatchedCount)
}
if got := StrictExitCode(report); got != StrictExitUnmatched {
t.Fatalf("StrictExitCode = %d, want %d", got, StrictExitUnmatched)
}
}
func TestReportRejectsNonReadOnlyCatalogEntries(t *testing.T) {
catalog := []Selector{
{
ID: "bad_write_selector",
Description: "invalid write selector",
AllowedUse: "write",
Required: Criteria{AutomationID: "frmMain", ControlType: "Window", FrameworkID: "WinForm"},
StabilityScore: 1,
Evidence: Evidence{CaptureID: "unit-test", UIAPath: "root"},
},
}
report := RunCatalogReport("fixture.json", loadN14Fixture(t), catalog, fixedReportTime())
if report.OK {
t.Fatalf("report OK=true for invalid catalog")
}
if report.Error == nil || report.Error.Code != "INVALID_CATALOG" {
t.Fatalf("report error = %#v, want INVALID_CATALOG", report.Error)
}
if got := StrictExitCode(report); got != StrictExitInvalidCatalog {
t.Fatalf("StrictExitCode = %d, want %d", got, StrictExitInvalidCatalog)
}
}
func TestReportDoesNotExposeVisibleText(t *testing.T) {
report := RunCatalogReport(
"testdata/n12r-2026-07-09-uia-redacted.json",
loadN14Fixture(t),
DefaultCatalog(),
fixedReportTime(),
)
body, err := RenderReportJSON(report)
if err != nil {
t.Fatalf("RenderReportJSON: %v", err)
}
jsonText := string(body)
for _, forbidden := range []string{`"name"`, `"value"`, "国网甘肃省电力公司"} {
if strings.Contains(jsonText, forbidden) {
t.Fatalf("JSON report exposed forbidden text %q in %s", forbidden, jsonText)
}
}
for _, required := range []string{`"has_name"`, `"name_length"`, `"selector_id"`, `"automation_id"`} {
if !strings.Contains(jsonText, required) {
t.Fatalf("JSON report missing required structural field %q in %s", required, jsonText)
}
}
}
func TestMarkdownReportDoesNotExposeVisibleText(t *testing.T) {
report := RunCatalogReport(
"testdata/n12r-2026-07-09-uia-redacted.json",
loadN14Fixture(t),
DefaultCatalog(),
fixedReportTime(),
)
markdown := RenderReportMarkdown(report)
for _, forbidden := range []string{"国网甘肃省电力公司", "| name |", "| value |"} {
if strings.Contains(markdown, forbidden) {
t.Fatalf("Markdown report exposed forbidden text %q in %s", forbidden, markdown)
}
}
for _, required := range []string{"# N14 UIA Selector Report", "## Summary", "automation_id", "control_type", "class_name", "has_name", "name_length"} {
if !strings.Contains(markdown, required) {
t.Fatalf("Markdown report missing %q in %s", required, markdown)
}
}
}
func TestStrictModeClassifiesAmbiguousMatches(t *testing.T) {
root := &Node{
ControlType: "Window",
AutomationID: "frmMain",
FrameworkID: "WinForm",
Children: []Node{
{ControlType: "Pane", AutomationID: "duplicate", FrameworkID: "WinForm"},
{ControlType: "Pane", AutomationID: "duplicate", FrameworkID: "WinForm"},
},
}
catalog := []Selector{
{
ID: "duplicate_panel",
Description: "duplicate structural selector",
AllowedUse: AllowedUseReadOnlyLocate,
Required: Criteria{AutomationID: "duplicate", ControlType: "Pane", FrameworkID: "WinForm"},
StabilityScore: 80,
Evidence: Evidence{CaptureID: "unit-test", UIAPath: "root/duplicate"},
},
}
report := RunCatalogReport("synthetic-ambiguous.json", root, catalog, fixedReportTime())
if !report.OK {
t.Fatalf("report OK=false: %#v", report.Error)
}
if report.CatalogSize != 1 {
t.Fatalf("catalog_size = %d, want 1", report.CatalogSize)
}
if report.MatchedCount != 1 {
t.Fatalf("matched_count = %d, want 1", report.MatchedCount)
}
if report.UnmatchedCount != 0 {
t.Fatalf("unmatched_count = %d, want 0", report.UnmatchedCount)
}
if report.AmbiguousCount != 1 {
t.Fatalf("ambiguous_count = %d, want 1", report.AmbiguousCount)
}
if len(report.Selectors) != 1 {
t.Fatalf("len(selectors) = %d, want 1", len(report.Selectors))
}
if got := report.Selectors[0].MatchCount; got != 2 {
t.Fatalf("selector match_count = %d, want 2", got)
}
if got := StrictExitCode(report); got != StrictExitAmbiguous {
t.Fatalf("StrictExitCode = %d, want %d", got, StrictExitAmbiguous)
}
}