auth-ui: Use Ory Kratos/Hydra v26.2.0 in E2E tests

Change-Id: I9301ccec0b634fef2a8b64e813ba9cec6c1b0b66
diff --git a/core/auth/ui/e2e/README.md b/core/auth/ui/e2e/README.md
index 5e2a8ac..710061d 100644
--- a/core/auth/ui/e2e/README.md
+++ b/core/auth/ui/e2e/README.md
@@ -7,8 +7,8 @@
 The harness accepts no `latest` or system-browser fallback:
 
 - Go **1.22 or newer** (the module directive is exactly `go 1.22`)
-- Kratos **v1.1.0**, SQLite release archive
-- Hydra **v2.2.0**, SQLite release archive
+- Kratos **v26.2.0**, SQLite release archive
+- Hydra **v26.2.0**, SQLite release archive
 - `github.com/mxschmitt/playwright-go` **v0.6100.0**
 - embedded Playwright CLI **1.61.1**
 - managed Chromium revision **1228**, Chromium **149.0.7827.55**
@@ -22,10 +22,12 @@
 
 | Platform | Kratos archive / SHA-256 | Hydra archive / SHA-256 |
 |---|---|---|
-| linux/amd64 | `kratos_1.1.0-linux_sqlite_64bit.tar.gz` / `6fb3077252dde7578c3100d2cd4eb52364ca6b3c1b0b76987e6d586e29008cbd` | `hydra_2.2.0-linux_sqlite_64bit.tar.gz` / `0fe0539fa452496ac5d98b558f93eb2dbb4cf43733da0b09f8f2bdb4445fc31e` |
-| linux/arm64 | `kratos_1.1.0-linux_sqlite_arm64.tar.gz` / `fde8a1a1aebd153baff88b1232e0c2a34fdaaafe90b5364f4ea580151e74898e` | `hydra_2.2.0-linux_sqlite_arm64.tar.gz` / `c499ffdaae0f2ab85eff0567214734515b741a393bef89115c16018f4dc0560d` |
-| darwin/amd64 | `kratos_1.1.0-macOS_sqlite_64bit.tar.gz` / `ebdc94f27cb6e6a3087ed756accfb7837465ac8e30af9433b4414101814f7769` | `hydra_2.2.0-macOS_sqlite_64bit.tar.gz` / `3d40ca8e99e2a6d840130928d5e0245212dba0eea9c26a0d7186ebb4382e673d` |
-| darwin/arm64 | `kratos_1.1.0-macOS_sqlite_arm64.tar.gz` / `6681d7b15dd04686d10764750ce3ad69672b3962553223399a3a315ba5370517` | `hydra_2.2.0-macOS_sqlite_arm64.tar.gz` / `89732ad1494c57ea39348f62dc5ef5c48de129cd205b17cb12bb67ad27094bb7` |
+| linux/amd64 | `kratos_26.2.0-linux_sqlite_64bit.tar.gz` / `c80113f2c861b2fbd80290697d1380000111e297716018617423cca7f2e7e668` | `hydra_26.2.0-linux_sqlite_64bit.tar.gz` / `28baf98c6d5da617c905d5ca77826fe863db9f9edab8f72664c69a56bde56b5a` |
+| linux/arm64 | `kratos_26.2.0-linux_sqlite_arm64.tar.gz` / `fd2cedebc4034ad1aef726f64665052c809fb05f86968e9c8a36372d7578e865` | `hydra_26.2.0-linux_sqlite_arm64.tar.gz` / `18e73ac632d2081b76fbceb03130803a63bdb9b8e2ecc3f06f4a6e6820cabe26` |
+| darwin/amd64 | `kratos_26.2.0-macOS_sqlite_64bit.tar.gz` / `0ac76d0ad85fe452e711a2aafe56b49b0f5d1a57200f4464dcb48e7267c94062` | `hydra_26.2.0-macOS_sqlite_64bit.tar.gz` / `ede453fa15c82cfb23e61b138a0b4d9558127a26ebf51eead97d0c58a7eff422` |
+| darwin/arm64 | `kratos_26.2.0-macOS_sqlite_arm64.tar.gz` / `e198d84bde6e4b7c68714cdad00c8800edd88c2311adde8b08ea5469cea263ec` | `hydra_26.2.0-macOS_sqlite_arm64.tar.gz` / `d45f26be758196e66e98dff06a9eaefcd21d719123f74f43a0f1fabcfb71da60` |
+
+The Kratos fixture declares `version: v26.2.0`; executable identity is independently enforced by archive checksum and binary version verification. These in-memory SQLite tests validate auth-ui compatibility, not production database migrations or rollback.
 
 These four Linux/macOS architecture combinations are the supported selection matrix. Windows and every other `GOOS/GOARCH` fail before downloads, builds, or service startup. Selection and compile checks do not constitute a native real-browser result. The complete native online/offline suite and automated artifact-format checks have been exercised on Linux amd64; Linux arm64 and both macOS rows remain pending and must not be described as empirically green.
 
diff --git a/core/auth/ui/e2e/archive_test.go b/core/auth/ui/e2e/archive_test.go
index c9c9c91..f41866e 100644
--- a/core/auth/ui/e2e/archive_test.go
+++ b/core/auth/ui/e2e/archive_test.go
@@ -82,13 +82,13 @@
 }
 
 func TestVersionOutputMatchesExactSemanticVersion(t *testing.T) {
-	for _, output := range []string{"Version:\tv1.1.0\n", "Version: 1.1.0\n"} {
-		if !versionOutputMatches([]byte(output), "v1.1.0") {
+	for _, output := range []string{"Version:\tv9.8.7\n", "Version: 9.8.7\n"} {
+		if !versionOutputMatches([]byte(output), "v9.8.7") {
 			t.Errorf("expected match for %q", output)
 		}
 	}
-	for _, output := range []string{"Version: v11.1.0", "Version: v1.1.01", "commit-v1.1.0-extra"} {
-		if versionOutputMatches([]byte(output), "v1.1.0") {
+	for _, output := range []string{"Version: v19.8.7", "Version: v9.8.70", "commit-v9.8.7-extra"} {
+		if versionOutputMatches([]byte(output), "v9.8.7") {
 			t.Errorf("unexpected match for %q", output)
 		}
 	}
diff --git a/core/auth/ui/e2e/artifacts.go b/core/auth/ui/e2e/artifacts.go
index fda096e..5b9a41b 100644
--- a/core/auth/ui/e2e/artifacts.go
+++ b/core/auth/ui/e2e/artifacts.go
@@ -18,8 +18,8 @@
 const (
 	maxOryArchiveDownloadSize int64 = 512 << 20
 
-	kratosVersion        = "v1.1.0"
-	hydraVersion         = "v2.2.0"
+	kratosVersion        = "v26.2.0"
+	hydraVersion         = "v26.2.0"
 	playwrightVersion    = "v0.6100.0"
 	playwrightCLIVersion = "1.61.1"
 	chromiumRevision     = "1228"
@@ -41,13 +41,13 @@
 }
 
 // Hashes are from the official release checksum manifests:
-// https://github.com/ory/kratos/releases/download/v1.1.0/checksums.txt
-// https://github.com/ory/hydra/releases/download/v2.2.0/checksums.txt
+// https://github.com/ory/kratos/releases/download/v26.2.0/checksums.txt
+// https://github.com/ory/hydra/releases/download/v26.2.0/checksums.txt
 var artifactPlatforms = map[string]platformArtifacts{
-	"linux/amd64":  platform("kratos_1.1.0-linux_sqlite_64bit.tar.gz", "6fb3077252dde7578c3100d2cd4eb52364ca6b3c1b0b76987e6d586e29008cbd", "hydra_2.2.0-linux_sqlite_64bit.tar.gz", "0fe0539fa452496ac5d98b558f93eb2dbb4cf43733da0b09f8f2bdb4445fc31e"),
-	"linux/arm64":  platform("kratos_1.1.0-linux_sqlite_arm64.tar.gz", "fde8a1a1aebd153baff88b1232e0c2a34fdaaafe90b5364f4ea580151e74898e", "hydra_2.2.0-linux_sqlite_arm64.tar.gz", "c499ffdaae0f2ab85eff0567214734515b741a393bef89115c16018f4dc0560d"),
-	"darwin/amd64": platform("kratos_1.1.0-macOS_sqlite_64bit.tar.gz", "ebdc94f27cb6e6a3087ed756accfb7837465ac8e30af9433b4414101814f7769", "hydra_2.2.0-macOS_sqlite_64bit.tar.gz", "3d40ca8e99e2a6d840130928d5e0245212dba0eea9c26a0d7186ebb4382e673d"),
-	"darwin/arm64": platform("kratos_1.1.0-macOS_sqlite_arm64.tar.gz", "6681d7b15dd04686d10764750ce3ad69672b3962553223399a3a315ba5370517", "hydra_2.2.0-macOS_sqlite_arm64.tar.gz", "89732ad1494c57ea39348f62dc5ef5c48de129cd205b17cb12bb67ad27094bb7"),
+	"linux/amd64":  platform("kratos_26.2.0-linux_sqlite_64bit.tar.gz", "c80113f2c861b2fbd80290697d1380000111e297716018617423cca7f2e7e668", "hydra_26.2.0-linux_sqlite_64bit.tar.gz", "28baf98c6d5da617c905d5ca77826fe863db9f9edab8f72664c69a56bde56b5a"),
+	"linux/arm64":  platform("kratos_26.2.0-linux_sqlite_arm64.tar.gz", "fd2cedebc4034ad1aef726f64665052c809fb05f86968e9c8a36372d7578e865", "hydra_26.2.0-linux_sqlite_arm64.tar.gz", "18e73ac632d2081b76fbceb03130803a63bdb9b8e2ecc3f06f4a6e6820cabe26"),
+	"darwin/amd64": platform("kratos_26.2.0-macOS_sqlite_64bit.tar.gz", "0ac76d0ad85fe452e711a2aafe56b49b0f5d1a57200f4464dcb48e7267c94062", "hydra_26.2.0-macOS_sqlite_64bit.tar.gz", "ede453fa15c82cfb23e61b138a0b4d9558127a26ebf51eead97d0c58a7eff422"),
+	"darwin/arm64": platform("kratos_26.2.0-macOS_sqlite_arm64.tar.gz", "e198d84bde6e4b7c68714cdad00c8800edd88c2311adde8b08ea5469cea263ec", "hydra_26.2.0-macOS_sqlite_arm64.tar.gz", "d45f26be758196e66e98dff06a9eaefcd21d719123f74f43a0f1fabcfb71da60"),
 }
 
 func platform(kratosArchive, kratosHash, hydraArchive, hydraHash string) platformArtifacts {
diff --git a/core/auth/ui/e2e/artifacts_test.go b/core/auth/ui/e2e/artifacts_test.go
index 89c8d77..088c963 100644
--- a/core/auth/ui/e2e/artifacts_test.go
+++ b/core/auth/ui/e2e/artifacts_test.go
@@ -43,22 +43,37 @@
 
 func TestOfficialArtifactHashes(t *testing.T) {
 	want := map[string]string{
-		"kratos_1.1.0-linux_sqlite_64bit.tar.gz": "6fb3077252dde7578c3100d2cd4eb52364ca6b3c1b0b76987e6d586e29008cbd",
-		"kratos_1.1.0-linux_sqlite_arm64.tar.gz": "fde8a1a1aebd153baff88b1232e0c2a34fdaaafe90b5364f4ea580151e74898e",
-		"kratos_1.1.0-macOS_sqlite_64bit.tar.gz": "ebdc94f27cb6e6a3087ed756accfb7837465ac8e30af9433b4414101814f7769",
-		"kratos_1.1.0-macOS_sqlite_arm64.tar.gz": "6681d7b15dd04686d10764750ce3ad69672b3962553223399a3a315ba5370517",
-		"hydra_2.2.0-linux_sqlite_64bit.tar.gz":  "0fe0539fa452496ac5d98b558f93eb2dbb4cf43733da0b09f8f2bdb4445fc31e",
-		"hydra_2.2.0-linux_sqlite_arm64.tar.gz":  "c499ffdaae0f2ab85eff0567214734515b741a393bef89115c16018f4dc0560d",
-		"hydra_2.2.0-macOS_sqlite_64bit.tar.gz":  "3d40ca8e99e2a6d840130928d5e0245212dba0eea9c26a0d7186ebb4382e673d",
-		"hydra_2.2.0-macOS_sqlite_arm64.tar.gz":  "89732ad1494c57ea39348f62dc5ef5c48de129cd205b17cb12bb67ad27094bb7",
+		"kratos_26.2.0-linux_sqlite_64bit.tar.gz": "c80113f2c861b2fbd80290697d1380000111e297716018617423cca7f2e7e668",
+		"kratos_26.2.0-linux_sqlite_arm64.tar.gz": "fd2cedebc4034ad1aef726f64665052c809fb05f86968e9c8a36372d7578e865",
+		"kratos_26.2.0-macOS_sqlite_64bit.tar.gz": "0ac76d0ad85fe452e711a2aafe56b49b0f5d1a57200f4464dcb48e7267c94062",
+		"kratos_26.2.0-macOS_sqlite_arm64.tar.gz": "e198d84bde6e4b7c68714cdad00c8800edd88c2311adde8b08ea5469cea263ec",
+		"hydra_26.2.0-linux_sqlite_64bit.tar.gz":  "28baf98c6d5da617c905d5ca77826fe863db9f9edab8f72664c69a56bde56b5a",
+		"hydra_26.2.0-linux_sqlite_arm64.tar.gz":  "18e73ac632d2081b76fbceb03130803a63bdb9b8e2ecc3f06f4a6e6820cabe26",
+		"hydra_26.2.0-macOS_sqlite_64bit.tar.gz":  "ede453fa15c82cfb23e61b138a0b4d9558127a26ebf51eead97d0c58a7eff422",
+		"hydra_26.2.0-macOS_sqlite_arm64.tar.gz":  "d45f26be758196e66e98dff06a9eaefcd21d719123f74f43a0f1fabcfb71da60",
 	}
+	seen := make(map[string]bool, len(want))
 	for _, platform := range artifactPlatforms {
 		for _, artifact := range []releaseArtifact{platform.Kratos, platform.Hydra} {
 			if want[artifact.Archive] != artifact.SHA256 {
 				t.Errorf("hash for %s=%s", artifact.Archive, artifact.SHA256)
 			}
+			if artifact.Version != "v26.2.0" {
+				t.Errorf("version for %s=%s", artifact.Archive, artifact.Version)
+			}
+			wantURL := "https://github.com/ory/" + artifact.Service + "/releases/download/v26.2.0/" + artifact.Archive
+			if artifact.URL != wantURL {
+				t.Errorf("URL for %s=%s, want %s", artifact.Archive, artifact.URL, wantURL)
+			}
+			if seen[artifact.Archive] {
+				t.Errorf("duplicate artifact %s", artifact.Archive)
+			}
+			seen[artifact.Archive] = true
 		}
 	}
+	if len(seen) != len(want) {
+		t.Errorf("artifact count=%d, want %d", len(seen), len(want))
+	}
 }
 
 func TestEnsureArchiveDownloadAndCache(t *testing.T) {
diff --git a/core/auth/ui/e2e/browser_artifacts_test.go b/core/auth/ui/e2e/browser_artifacts_test.go
index 5825dcf..c51766b 100644
--- a/core/auth/ui/e2e/browser_artifacts_test.go
+++ b/core/auth/ui/e2e/browser_artifacts_test.go
@@ -6,7 +6,6 @@
 	"archive/zip"
 	"bytes"
 	"encoding/json"
-	"fmt"
 	"os"
 	"path/filepath"
 	"strings"
@@ -36,7 +35,7 @@
 			t.Fatalf("health check %s: %v", sanitizeFinalURL(endpoint), err)
 		}
 	}
-	if kratosVersion != "v1.1.0" || hydraVersion != "v2.2.0" || playwrightVersion != "v0.6100.0" {
+	if kratosVersion != "v26.2.0" || hydraVersion != "v26.2.0" || playwrightVersion != "v0.6100.0" {
 		t.Fatalf("unexpected pinned versions: %s %s %s", kratosVersion, hydraVersion, playwrightVersion)
 	}
 	if got := testBrowser.Browser.Version(); got != chromiumVersion {
@@ -55,7 +54,7 @@
 			t.Fatalf("%s reported a wildcard listener", service)
 		}
 		for _, port := range ports {
-			if !strings.Contains(string(log), fmt.Sprintf("http server on 127.0.0.1:%d", port)) && !strings.Contains(string(log), fmt.Sprintf("httpd on: 127.0.0.1:%d", port)) {
+			if !listenerLogReportsLoopback(string(log), port) {
 				t.Fatalf("%s did not report loopback listener port %d", service, port)
 			}
 		}
diff --git a/core/auth/ui/e2e/browser_test.go b/core/auth/ui/e2e/browser_test.go
index 3a42eb3..9422096 100644
--- a/core/auth/ui/e2e/browser_test.go
+++ b/core/auth/ui/e2e/browser_test.go
@@ -10,6 +10,98 @@
 	"time"
 )
 
+func listenerLogReportsLoopback(log string, port int) bool {
+	if strings.Contains(log, "0.0.0.0:") {
+		return false
+	}
+
+	address := fmt.Sprintf("127.0.0.1:%d", port)
+	for _, line := range strings.Split(log, "\n") {
+		if logLineContainsPhrase(line, "http server on "+address) || logLineContainsPhrase(line, "httpd on: "+address) {
+			return true
+		}
+
+		structuredStartup := logLineContainsFieldPhrase(line, `msg="Starting the public httpd"`) ||
+			logLineContainsFieldPhrase(line, `msg="Starting the admin httpd"`) ||
+			logLineContainsFieldPhrase(line, "msg=Starting the public httpd") ||
+			logLineContainsFieldPhrase(line, "msg=Starting the admin httpd")
+		if structuredStartup && logLineHasField(line, "addr", address) {
+			return true
+		}
+	}
+	return false
+}
+
+func logLineContainsPhrase(line, phrase string) bool {
+	for start := 0; start < len(line); {
+		index := strings.Index(line[start:], phrase)
+		if index < 0 {
+			return false
+		}
+		end := start + index + len(phrase)
+		if end == len(line) || line[end] == ' ' || line[end] == '\t' {
+			return true
+		}
+		start += index + 1
+	}
+	return false
+}
+
+func logLineContainsFieldPhrase(line, phrase string) bool {
+	for start := 0; start < len(line); {
+		index := strings.Index(line[start:], phrase)
+		if index < 0 {
+			return false
+		}
+		begin := start + index
+		end := begin + len(phrase)
+		beginBoundary := begin == 0 || line[begin-1] == ' ' || line[begin-1] == '\t'
+		endBoundary := end == len(line) || line[end] == ' ' || line[end] == '\t'
+		if beginBoundary && endBoundary {
+			return true
+		}
+		start = begin + 1
+	}
+	return false
+}
+
+func logLineHasField(line, name, value string) bool {
+	plain, quoted := name+"="+value, name+`="`+value+`"`
+	for _, field := range strings.Fields(line) {
+		if field == plain || field == quoted {
+			return true
+		}
+	}
+	return false
+}
+
+func TestListenerLogReportsLoopback(t *testing.T) {
+	tests := []struct {
+		name string
+		log  string
+		want bool
+	}{
+		{name: "legacy Kratos", log: "level=info msg=Running http server on 127.0.0.1:4433", want: true},
+		{name: "legacy Hydra", log: "time=now httpd on: 127.0.0.1:4433", want: true},
+		{name: "structured public quoted", log: `level=info msg="Starting the public httpd" addr="127.0.0.1:4433"`, want: true},
+		{name: "structured admin unquoted", log: "level=info msg=Starting the admin httpd addr=127.0.0.1:4433", want: true},
+		{name: "wrong structured port", log: `level=info msg="Starting the public httpd" addr=127.0.0.1:4434`},
+		{name: "wrong legacy port", log: "level=info httpd on: 127.0.0.1:44330"},
+		{name: "wildcard", log: `level=info msg="Starting the public httpd" addr=0.0.0.0:4433`},
+		{name: "split structured evidence", log: "level=info msg=Starting the public httpd\nlevel=info addr=127.0.0.1:4433"},
+		{name: "unrelated event", log: "level=info msg=Starting metrics addr=127.0.0.1:4433"},
+		{name: "address in unrelated field", log: `level=info msg="Starting the public httpd" target=127.0.0.1:4433`},
+		{name: "message in unrelated field", log: `diagnosticmsg="Starting the public httpd" addr=127.0.0.1:4433`},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got := listenerLogReportsLoopback(test.log, 4433); got != test.want {
+				t.Fatalf("listenerLogReportsLoopback()=%v, want %v for %q", got, test.want, test.log)
+			}
+		})
+	}
+}
+
 func TestLoopbackRoutePolicy(t *testing.T) {
 	policy := newRoutePolicy([]string{"http://127.0.0.1:10001", "http://127.0.0.1:10002"})
 	for _, raw := range []string{"http://127.0.0.1:10001/login?flow=secret", "http://127.0.0.1:10002/static/main.css"} {
diff --git a/core/auth/ui/e2e/config_test.go b/core/auth/ui/e2e/config_test.go
index a1cf000..7e74723 100644
--- a/core/auth/ui/e2e/config_test.go
+++ b/core/auth/ui/e2e/config_test.go
@@ -45,7 +45,7 @@
 		}
 	}
 	kratos, _ := os.ReadFile(filepath.Join(first, "kratos.yml"))
-	for _, required := range []string{"dsn: memory", "default_schema_id: user", "id: user", "password:\n      enabled: true", "hook: session", "leak_sensitive_values: false", "smtp://e2e:e2e@127.0.0.1:11007", "http://127.0.0.1:11005/login", "file://"} {
+	for _, required := range []string{"version: v26.2.0", "dsn: memory", "default_schema_id: user", "id: user", "password:\n      enabled: true", "hook: session", "leak_sensitive_values: false", "smtp://e2e:e2e@127.0.0.1:11007", "http://127.0.0.1:11005/login", "file://"} {
 		if !strings.Contains(string(kratos), required) {
 			t.Errorf("kratos config missing %q", required)
 		}
diff --git a/core/auth/ui/e2e/testdata/kratos.yml.tmpl b/core/auth/ui/e2e/testdata/kratos.yml.tmpl
index 820786a..d175b77 100644
--- a/core/auth/ui/e2e/testdata/kratos.yml.tmpl
+++ b/core/auth/ui/e2e/testdata/kratos.yml.tmpl
@@ -1,4 +1,4 @@
-version: v1.1.0
+version: v26.2.0
 dsn: memory
 
 serve: