Version bump and some lable/relation fixes.
This commit is contained in:
1 parent
d717b93849
commit
396efbd7c5
15 files changed
+474
-8
No files matched your search
@@ -8,6 +8,46 @@ static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret,
|
||||
static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot);
|
||||
static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining);
|
||||
|
||||
// Some native entries leave Name empty and expose their label only through
|
||||
// LABELLED_BY. Follow explicit relationships, never nearby text or field values.
|
||||
static gchar *cthulhu_text_control_label(AtspiAccessible *source) {
|
||||
GArray *relations = atspi_accessible_get_relation_set(source, NULL);
|
||||
if (relations == NULL) return NULL;
|
||||
GString *result = g_string_new("");
|
||||
guint remaining = 16;
|
||||
for (guint i = 0; i < relations->len && remaining > 0; i++) {
|
||||
AtspiRelation *relation = g_array_index(relations, AtspiRelation *, i);
|
||||
if (atspi_relation_get_relation_type(relation) != ATSPI_RELATION_LABELLED_BY) continue;
|
||||
gint targets = atspi_relation_get_n_targets(relation);
|
||||
for (gint j = 0; j < targets && remaining > 0; j++, remaining--) {
|
||||
AtspiAccessible *label = atspi_relation_get_target(relation, j);
|
||||
if (label == NULL) continue;
|
||||
gchar *name = atspi_accessible_get_name(label, NULL);
|
||||
if ((name == NULL || *name == '\0') &&
|
||||
atspi_accessible_get_role(label, NULL) == ATSPI_ROLE_LABEL) {
|
||||
AtspiText *text = atspi_accessible_get_text(label);
|
||||
if (text != NULL) {
|
||||
gint count = atspi_text_get_character_count(text, NULL);
|
||||
g_free(name);
|
||||
name = count > 0 ? atspi_text_get_text(text, 0, MIN(count, 1024), NULL) : NULL;
|
||||
g_object_unref(text);
|
||||
}
|
||||
}
|
||||
if (name != NULL && *g_strstrip(name) != '\0' &&
|
||||
result->len + strlen(name) + 1 <= 4096) {
|
||||
if (result->len > 0) g_string_append_c(result, ' ');
|
||||
g_string_append(result, name);
|
||||
}
|
||||
g_free(name);
|
||||
g_object_unref(label);
|
||||
}
|
||||
}
|
||||
for (guint i = 0; i < relations->len; i++)
|
||||
g_object_unref(g_array_index(relations, AtspiRelation *, i));
|
||||
g_array_unref(relations);
|
||||
return g_string_free(result, FALSE);
|
||||
}
|
||||
|
||||
// Collect only the selected row. Walking an entire table on every arrow key
|
||||
// would block input in large application tables.
|
||||
gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns,
|
||||
@@ -389,6 +429,12 @@ cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean capt
|
||||
g_error_free(error);
|
||||
error = NULL;
|
||||
}
|
||||
if (!snapshot.defunct && (snapshot.name == NULL || *snapshot.name == '\0') &&
|
||||
(g_strcmp0(snapshot.role, "text") == 0 || g_strcmp0(snapshot.role, "entry") == 0 ||
|
||||
g_strcmp0(snapshot.role, "password text") == 0)) {
|
||||
g_free(snapshot.name);
|
||||
snapshot.name = cthulhu_text_control_label(source);
|
||||
}
|
||||
cthulhu_capture_menu_checkability(source, &snapshot);
|
||||
if (!snapshot.defunct && (g_strcmp0(snapshot.role, "list") == 0 ||
|
||||
g_strcmp0(snapshot.role, "list box") == 0)) {
|
||||
@@ -556,7 +602,7 @@ static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret,
|
||||
g_object_unref(text);
|
||||
return NULL;
|
||||
}
|
||||
AtspiTextRange *range = atspi_text_get_string_at_offset(
|
||||
AtspiTextRange *range = cthulhu_text_get_string_at_offset(
|
||||
text, caretOffset, ATSPI_TEXT_GRANULARITY_LINE, &error);
|
||||
// AXText.get_line_at_offset retries Chromium's invalid end-of-text
|
||||
// range. A valid empty range is a real blank line and must stay empty.
|
||||
@@ -568,7 +614,7 @@ static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret,
|
||||
}
|
||||
if (retry) {
|
||||
g_boxed_free(atspi_text_range_get_type(), range);
|
||||
range = atspi_text_get_string_at_offset(
|
||||
range = cthulhu_text_get_string_at_offset(
|
||||
text, caretOffset - 1, ATSPI_TEXT_GRANULARITY_LINE, &error);
|
||||
}
|
||||
}
|
||||
@@ -836,7 +882,7 @@ gchar *cthulhu_capture_caret_unit(AtspiAccessible *source, AtspiTextGranularity
|
||||
g_object_unref(text);
|
||||
return NULL;
|
||||
}
|
||||
AtspiTextRange *range = atspi_text_get_string_at_offset(
|
||||
AtspiTextRange *range = cthulhu_text_get_string_at_offset(
|
||||
text, caretOffset, unit, &error);
|
||||
g_object_unref(text);
|
||||
if (error != NULL) {
|
||||
|
||||
@@ -101,6 +101,8 @@ void cthulhu_capture_set_position(AtspiAccessible *source, GHashTable *attribute
|
||||
gboolean cthulhu_has_set_position(const gchar *role);
|
||||
gchar *cthulhu_live_content(AtspiAccessible *source);
|
||||
gchar *cthulhu_capture_caret_unit(AtspiAccessible *source, AtspiTextGranularity unit);
|
||||
AtspiTextRange *cthulhu_text_get_string_at_offset(AtspiText *text, gint offset,
|
||||
AtspiTextGranularity unit, GError **error);
|
||||
guint start_foreground_timer(gpointer data);
|
||||
const gchar *cthulhu_event_type(AtspiEvent *event);
|
||||
gint cthulhu_event_detail1(AtspiEvent *event);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "adapter_native.h"
|
||||
#include <string.h>
|
||||
|
||||
AtspiTextRange *cthulhu_text_get_string_at_offset(AtspiText *text, gint offset,
|
||||
AtspiTextGranularity unit, GError **error) {
|
||||
if (unit != ATSPI_TEXT_GRANULARITY_LINE || offset < 0) {
|
||||
return atspi_text_get_string_at_offset(text, offset, unit, error);
|
||||
}
|
||||
|
||||
// Chromium can retain layout boundaries past truncated hypertext. In a
|
||||
// captured crash, LINE at the final U+2026 (140 of 141 characters) tried
|
||||
// to take a substring starting at 146 and aborted the entire browser.
|
||||
// Checking returned boundaries is too late: use bounded GetText for an
|
||||
// ellipsis before invoking the provider's boundary calculation.
|
||||
GError *probeError = NULL;
|
||||
guint character = atspi_text_get_character_at_offset(text, offset, &probeError);
|
||||
if (probeError != NULL) {
|
||||
g_propagate_error(error, probeError);
|
||||
return NULL;
|
||||
}
|
||||
if (character != 0x2026) {
|
||||
return atspi_text_get_string_at_offset(text, offset, unit, error);
|
||||
}
|
||||
gint count = atspi_text_get_character_count(text, &probeError);
|
||||
if (probeError != NULL) {
|
||||
g_propagate_error(error, probeError);
|
||||
return NULL;
|
||||
}
|
||||
if (count <= 0 || offset != count - 1) {
|
||||
return atspi_text_get_string_at_offset(text, offset, unit, error);
|
||||
}
|
||||
|
||||
gint start = MAX(0, offset - 4095);
|
||||
gchar *content = atspi_text_get_text(text, start, offset + 1, error);
|
||||
if (content == NULL) return NULL;
|
||||
if (!g_utf8_validate(content, -1, NULL) ||
|
||||
g_utf8_strlen(content, -1) != offset + 1 - start ||
|
||||
!g_str_has_suffix(content, "\342\200\246")) {
|
||||
g_free(content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Preserve explicit line breaks. A provider's visual wrapping is unsafe
|
||||
// at this character; retain the bounded logical line and native offsets.
|
||||
gchar *line = strrchr(content, '\n');
|
||||
line = line != NULL ? line + 1 : content;
|
||||
AtspiTextRange *range = g_new0(AtspiTextRange, 1);
|
||||
range->start_offset = start + g_utf8_pointer_to_offset(content, line);
|
||||
range->end_offset = offset + 1;
|
||||
range->content = g_strdup(line);
|
||||
g_free(content);
|
||||
return range;
|
||||
}
|
||||
@@ -62,4 +62,33 @@ func TestNativeBrowserLogicalLines(t *testing.T) {
|
||||
t.Fatalf("preserved %s line = %+v, %v", test.name, line, err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"Truncated preview…", "猫🐈…", "…"} {
|
||||
ref := findNamedReference(t, tree, name)
|
||||
if !ref.Valid() {
|
||||
t.Fatalf("ellipsis object %q missing", name)
|
||||
}
|
||||
offset := len([]rune(name)) - 1
|
||||
for _, unit := range []DocumentCaretUnit{DocumentCaretLine, DocumentCaretBrailleLine, DocumentCaretLineRemainder} {
|
||||
line, err := adapter.ReadDocumentCaret(ctx, ref, offset, unit)
|
||||
if err != nil || line.End != offset+1 || !strings.HasSuffix(line.Text, "…") {
|
||||
t.Fatalf("ellipsis %q unit %d = %+v, %v", name, unit, line, err)
|
||||
}
|
||||
}
|
||||
if err := adapter.SetDocumentCaretPosition(ctx, ref, focus, offset); err != nil {
|
||||
t.Fatalf("place caret on ellipsis: %v", err)
|
||||
}
|
||||
// Drain the actual native caret event, exercising snapshot text,
|
||||
// character/word capture and logical-line repair before proceeding.
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if event.Type == CaretEvent && strings.HasSuffix(event.Source.BrailleLine, "…") {
|
||||
goto caretCaptured
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("ellipsis caret event missing: ", ctx.Err())
|
||||
}
|
||||
}
|
||||
caretCaptured:
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func nativePointText(ctx context.Context, source, window *C.AtspiAccessible, x,
|
||||
}
|
||||
read := func(granularity C.AtspiTextGranularity) (string, int) {
|
||||
var err *C.GError
|
||||
value := C.atspi_text_get_string_at_offset(text, offset, granularity, &err)
|
||||
value := C.cthulhu_text_get_string_at_offset(text, offset, granularity, &err)
|
||||
if err != nil {
|
||||
C.g_error_free(err)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (adapter *Adapter) nativeTransferItem(ctx context.Context, object ObjectID,
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
line := C.atspi_text_get_string_at_offset(text, C.gint(offset), C.ATSPI_TEXT_GRANULARITY_LINE, nil)
|
||||
line := C.cthulhu_text_get_string_at_offset(text, C.gint(offset), C.ATSPI_TEXT_GRANULARITY_LINE, nil)
|
||||
if line == nil {
|
||||
return TransferItem{}, errors.New("focused line unavailable")
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "adapter_native.h"
|
||||
#include <string.h>
|
||||
|
||||
static const gchar *content;
|
||||
static guint boundaryCalls, characterCalls, textCalls;
|
||||
static gboolean failProbe, changedText;
|
||||
|
||||
guint atspi_text_get_character_at_offset(AtspiText *text, gint offset, GError **error) {
|
||||
characterCalls++;
|
||||
if (failProbe) {
|
||||
g_set_error_literal(error, g_quark_from_static_string("probe"), 1, "removed text");
|
||||
return 0;
|
||||
}
|
||||
return offset >= 0 && offset < g_utf8_strlen(content, -1)
|
||||
? g_utf8_get_char(g_utf8_offset_to_pointer(content, offset)) : 0;
|
||||
}
|
||||
|
||||
gint atspi_text_get_character_count(AtspiText *text, GError **error) {
|
||||
return g_utf8_strlen(content, -1);
|
||||
}
|
||||
|
||||
gchar *atspi_text_get_text(AtspiText *text, gint start, gint end, GError **error) {
|
||||
textCalls++;
|
||||
if (changedText) return g_strdup("");
|
||||
g_assert_cmpint(start, >=, 0);
|
||||
g_assert_cmpint(end - start, <=, 4096);
|
||||
g_assert_cmpint(end, <=, g_utf8_strlen(content, -1));
|
||||
const gchar *first = g_utf8_offset_to_pointer(content, start);
|
||||
return g_strndup(first, g_utf8_offset_to_pointer(content, end) - first);
|
||||
}
|
||||
|
||||
AtspiTextRange *atspi_text_get_string_at_offset(AtspiText *text, gint offset,
|
||||
AtspiTextGranularity unit, GError **error) {
|
||||
boundaryCalls++;
|
||||
// Model the verified browser failure: asking for LINE at the final
|
||||
// ellipsis aborts inside the provider, before it can return a bad range.
|
||||
if (unit == ATSPI_TEXT_GRANULARITY_LINE && offset >= 0 &&
|
||||
offset == g_utf8_strlen(content, -1) - 1) {
|
||||
g_assert_cmpuint(g_utf8_get_char(g_utf8_offset_to_pointer(content, offset)), !=, 0x2026);
|
||||
}
|
||||
AtspiTextRange *range = g_new0(AtspiTextRange, 1);
|
||||
range->end_offset = g_utf8_strlen(content, -1);
|
||||
range->content = g_strdup(content);
|
||||
return range;
|
||||
}
|
||||
|
||||
static void assert_range(const gchar *value, gint offset, AtspiTextGranularity unit,
|
||||
gint start, gint end, const gchar *expected, guint calls) {
|
||||
content = value;
|
||||
boundaryCalls = characterCalls = textCalls = 0;
|
||||
GError *error = NULL;
|
||||
AtspiTextRange *range = cthulhu_text_get_string_at_offset(NULL, offset, unit, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_nonnull(range);
|
||||
g_assert_cmpint(range->start_offset, ==, start);
|
||||
g_assert_cmpint(range->end_offset, ==, end);
|
||||
g_assert_cmpstr(range->content, ==, expected);
|
||||
g_assert_cmpuint(boundaryCalls, ==, calls);
|
||||
if (unit != ATSPI_TEXT_GRANULARITY_LINE || offset < 0) g_assert_cmpuint(characterCalls, ==, 0);
|
||||
g_assert_cmpuint(textCalls, ==, calls == 0 ? 1 : 0);
|
||||
g_free(range->content);
|
||||
g_free(range);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assert_range("Truncated text…", 14, ATSPI_TEXT_GRANULARITY_LINE, 0, 15, "Truncated text…", 0);
|
||||
assert_range("…", 0, ATSPI_TEXT_GRANULARITY_LINE, 0, 1, "…", 0);
|
||||
assert_range("First\n猫🐈…", 8, ATSPI_TEXT_GRANULARITY_LINE, 6, 9, "猫🐈…", 0);
|
||||
assert_range("Ordinary text", 12, ATSPI_TEXT_GRANULARITY_LINE, 0, 13, "Ordinary text", 1);
|
||||
assert_range("An… inside", 2, ATSPI_TEXT_GRANULARITY_LINE, 0, 10, "An… inside", 1);
|
||||
assert_range("Text…", 4, ATSPI_TEXT_GRANULARITY_CHAR, 0, 5, "Text…", 1);
|
||||
assert_range("Text…", 4, ATSPI_TEXT_GRANULARITY_WORD, 0, 5, "Text…", 1);
|
||||
assert_range("Text…", -1, ATSPI_TEXT_GRANULARITY_LINE, 0, 5, "Text…", 1);
|
||||
gchar *prefix = g_strnfill(5000, 'x');
|
||||
gchar *longText = g_strconcat(prefix, "…", NULL);
|
||||
assert_range(longText, 5000, ATSPI_TEXT_GRANULARITY_LINE, 905, 5001, longText + 905, 0);
|
||||
g_free(prefix);
|
||||
g_free(longText);
|
||||
|
||||
content = "Changed…";
|
||||
boundaryCalls = 0;
|
||||
changedText = TRUE;
|
||||
GError *error = NULL;
|
||||
g_assert_null(cthulhu_text_get_string_at_offset(NULL, 7, ATSPI_TEXT_GRANULARITY_LINE, &error));
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpuint(boundaryCalls, ==, 0);
|
||||
changedText = FALSE;
|
||||
failProbe = TRUE;
|
||||
g_assert_null(cthulhu_text_get_string_at_offset(NULL, 7, ATSPI_TEXT_GRANULARITY_LINE, &error));
|
||||
g_assert_nonnull(error);
|
||||
g_assert_cmpuint(boundaryCalls, ==, 0);
|
||||
g_clear_error(&error);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTextBoundaryAvoidsProviderEllipsisAbort(t *testing.T) {
|
||||
flags, err := exec.Command("pkg-config", "--cflags", "--libs", "atspi-2").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("native text dependencies: %v: %s", err, flags)
|
||||
}
|
||||
binary := filepath.Join(t.TempDir(), "text-boundary")
|
||||
args := []string{"-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-I.",
|
||||
"adapter_text_boundary.c", "testdata/text_boundary.c", "-o", binary}
|
||||
args = append(args, strings.Fields(string(flags))...)
|
||||
if output, err := exec.Command("cc", args...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("compile native boundary regression: %v: %s", err, output)
|
||||
}
|
||||
if output, err := exec.Command(binary).CombinedOutput(); err != nil {
|
||||
t.Fatalf("native boundary regression: %v: %s", err, output)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package atspi
|
||||
|
||||
/*
|
||||
#cgo pkg-config: atspi-2
|
||||
#include <atspi/atspi.h>
|
||||
#include "adapter_native.h"
|
||||
#include <string.h>
|
||||
// Bound the Go copy even when an application returns an oversized line.
|
||||
static int review_text_length(const gchar *text, int limit) {
|
||||
@@ -176,7 +176,7 @@ func (source nativeReviewText) line(ctx context.Context, offset int) (TextRange,
|
||||
return TextRange{}, err
|
||||
}
|
||||
var nativeErr *C.GError
|
||||
line := C.atspi_text_get_string_at_offset(source.text, C.gint(offset), C.ATSPI_TEXT_GRANULARITY_LINE, &nativeErr)
|
||||
line := C.cthulhu_text_get_string_at_offset(source.text, C.gint(offset), C.ATSPI_TEXT_GRANULARITY_LINE, &nativeErr)
|
||||
if line != nil {
|
||||
defer C.g_boxed_free(C.atspi_text_range_get_type(), C.gpointer(line))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
func TestNativeTextLabelRelations(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_TEXT_LABELS") != "1" {
|
||||
t.Skip("requires private GTK/AT-SPI session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
adapter := atspi.New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
fixture := exec.CommandContext(ctx, "/usr/bin/python3", "-u", "../../tests/text_label_fixture.py")
|
||||
fixture.Env = append(os.Environ(), "NO_AT_BRIDGE=0", "GTK_MODULES=gail:atk-bridge")
|
||||
stdin, err := fixture.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stdout, err := fixture.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fixture.Stderr = os.Stderr
|
||||
if err := fixture.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { stdin.Close(); fixture.Wait() }()
|
||||
if scanner := bufio.NewScanner(stdout); !scanner.Scan() || scanner.Text() != "ready" {
|
||||
t.Fatal("fixture did not start")
|
||||
}
|
||||
var previous atspi.ObjectID
|
||||
for index, probe := range []struct{ name, value string }{
|
||||
{"Destination:", ""},
|
||||
{"Display name:", "Example document"},
|
||||
{"Preferred name", "Example value"},
|
||||
{"Secret:", ""},
|
||||
{"", "Unlabeled value"},
|
||||
{"Account identifier", ""},
|
||||
} {
|
||||
if _, err := fmt.Fprintln(stdin, index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFocus:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("missing entry focus", index)
|
||||
case event := <-adapter.Events():
|
||||
if (event.Type != atspi.FocusEvent && event.Type != atspi.FocusedStateEvent) || !event.Source.Focused {
|
||||
continue
|
||||
}
|
||||
snapshot := event.Source
|
||||
if snapshot.Role != "text" && snapshot.Role != "password text" {
|
||||
continue
|
||||
}
|
||||
if snapshot.ID() == previous {
|
||||
continue
|
||||
}
|
||||
previous = snapshot.ID()
|
||||
if snapshot.Name != probe.name {
|
||||
t.Errorf("entry %d name = %q, want %q", index, snapshot.Name, probe.name)
|
||||
}
|
||||
speech := FocusText(snapshot)
|
||||
for _, part := range []string{probe.name, probe.value} {
|
||||
if part != "" && !strings.Contains(speech, part) {
|
||||
t.Errorf("entry %d speech %q omits %q", index, speech, part)
|
||||
}
|
||||
}
|
||||
if strings.Contains(speech, "private fixture value") {
|
||||
t.Error("password content leaked into speech")
|
||||
}
|
||||
break waitFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import "strings"
|
||||
|
||||
const Name = "Cthulhu"
|
||||
|
||||
const Version = "2026.10.17"
|
||||
const Version = "2026.10.18"
|
||||
const Channel = "master"
|
||||
|
||||
var Build = Version
|
||||
|
||||
Reference in new issue
Block a user