aboutsummaryrefslogtreecommitdiff
path: root/spike/settings/test-strings/make_test_strings.py
blob: 9632220745c56a508e4c1ac15cb88be2c24e7546 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env python3
# Author:  cirakg & claude ai
# Summary: Generate the settings-spike test strings: one .txt per string and an index.html with copy buttons.
"""Generate the test strings for the M1 settings spike (docs/SETTINGS_SPIKE.md).

Length strings look like
    N=1000|abcdef...P0050...P0100...xyzEND
"N=<length>|" says how long the string should be, a marker "P<position>"
sits at every 50th character, and "END" closes it. If the phone app cuts a
string, the watch shows a wrong length and no "END"; the last marker it shows
tells roughly where the cut happened. Only letters, digits, "=", "|" are
used, so the length tests don't also test character handling.

special.txt holds the characters that may be rejected (& < > \\ " { } %),
non-ASCII text, a fake otpauth URI and a JSON fragment.

The phone refused to save special.txt (spike run 2026-10-21), so the c-*.txt
strings split it into one character group each, and aegis-json.txt is a
complete fake unencrypted Aegis export: together they show which characters
block saving and whether an Aegis file can be pasted at all.

Usage: python3 make_test_strings.py   (writes into its own folder)
"""

from __future__ import annotations

import html
from pathlib import Path

LENGTHS = (100, 255, 256, 500, 800, 801, 1000, 2000, 4000, 8000)
SPECIAL = (
    'special: & < > \\ " { } % | non-ASCII: äöü ß € 😀 | '
    "otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&issuer=Fake&digits=6 | "
    '{"name":"Fake","path":"a\\/b"} END'
)
# Test C, split: (file name, label, string). Each ends with " END".
SPLIT = (
    ("c-1-percent", "C1: percent", "C1 % 100% END"),
    ("c-2-angle", "C2: angle brackets", "C2 < > <b> END"),
    ("c-3-backslash", "C3: backslash", "C3 \\ a\\/b END"),
    ("c-4-quote", "C4: double quote", 'C4 " "x" END'),
    ("c-5-braces", "C5: braces", "C5 { } {x} END"),
    ("c-6-latin", "C6: accented letters and euro sign", "C6 äöü ß € END"),
    ("c-7-emoji", "C7: emoji", "C7 😀 END"),
    ("c-8-json", "C8: JSON fragment", 'C8 {"name":"Fake","n":1} END'),
)
AEGIS_JSON = (
    '{"version":1,"header":{"slots":null,"params":null},"db":{"version":3,"entries":['
    '{"type":"totp","uuid":"00000000-0000-4000-8000-000000000001","name":"alice@example.com",'
    '"issuer":"TestOne","note":"","icon":null,"info":{"secret":"JBSWY3DPEHPK3PXP",'
    '"algo":"SHA1","digits":6,"period":30}}]}}'
)
HEADER = (
    "# Author:  cirakg & claude ai\n# Summary: {summary}\n"
    "# Copy ONLY the last line (the test string) into a settings field.\n\n"
)
LETTERS = "abcdefghijklmnopqrstuvwxyz"


def marker_string(length: int) -> str:
    """A string of exactly `length` characters with position markers."""
    text = f"N={length}|"
    i = 0
    while len(text) < length - 3:
        pos = len(text)
        if pos % 50 == 0 and pos + 5 <= length - 3:
            text += f"P{pos:04d}"
        else:
            text += LETTERS[i % len(LETTERS)]
            i += 1
    text += "END"
    assert len(text) == length, (length, len(text))
    return text


def main() -> None:
    here = Path(__file__).resolve().parent
    entries = [(f"len-{n:04d}", f"{n} characters", marker_string(n)) for n in LENGTHS]
    entries.append(("special", "special characters", SPECIAL))
    entries.extend(SPLIT)
    entries.append(("aegis-json", "fake unencrypted Aegis export", AEGIS_JSON))
    for name, label, value in entries:
        (here / f"{name}.txt").write_text(HEADER.format(summary=f"Settings spike test string: {label}.") + value + "\n")
    cards = "\n".join(
        f"<section><h2>{html.escape(label)}</h2>"
        f'<textarea id="{name}" readonly rows="3">{html.escape(value)}</textarea>'
        f"<button onclick=\"copyText('{name}')\">Copy</button></section>"
        for name, label, value in entries
    )
    page = f"""<!doctype html>
<!-- Author:  cirakg & claude ai -->
<!-- Summary: Settings spike test strings with copy buttons; open on the phone, copy, paste into a settings field. -->
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Settings spike test strings</title>
<style>body{{font-family:sans-serif;margin:16px}}textarea{{width:100%;font-family:monospace}}
button{{font-size:1.1em;padding:6px 16px;margin:4px 0 16px}}</style></head>
<body><h1>Settings spike test strings</h1>
<p>Tap Copy, then paste into one of the Text fields in the app settings. Expected lengths are in each heading.</p>
{cards}
<script>
function copyText(id) {{
  const area = document.getElementById(id);
  area.select();
  const done = () => alert("Copied " + area.value.length + " characters");
  if (navigator.clipboard && window.isSecureContext) {{
    navigator.clipboard.writeText(area.value).then(done, () => {{ document.execCommand("copy"); done(); }});
  }} else {{ document.execCommand("copy"); done(); }}
}}
</script></body></html>
"""
    (here / "index.html").write_text(page)
    print(f"wrote {len(entries)} test strings and index.html into {here}")


if __name__ == "__main__":
    main()