From 52d1698221f7c6af8d81ef42aed58921914bade6 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 18 Sep 2026 08:09:22 +1000 Subject: [PATCH] DOCS+BE: skill updates, write_notifications fix #1793 --- .claude/skills/install-scripts/SKILL.md | 28 +++++++++++++ .claude/skills/pr-analysis/SKILL.md | 12 ++++++ .gemini/skills/install-scripts/SKILL.md | 28 +++++++++++++ .gemini/skills/pr-analysis/SKILL.md | 12 ++++++ .gemini/skills/skills-index/SKILL.md | 5 ++- .github/skills/devcontainer-configs/SKILL.md | 8 ++++ .github/skills/install-scripts/SKILL.md | 28 +++++++++++++ .github/skills/pr-analysis/SKILL.md | 12 ++++++ .github/skills/skills-overview/SKILL.md | 5 ++- scripts/check_skill_pairs.py | 1 + server/api_server/openapi/schemas.py | 14 ++++++- .../test_messaging_in_app_endpoints.py | 40 +++++++++++++++++++ 12 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/install-scripts/SKILL.md create mode 100644 .gemini/skills/install-scripts/SKILL.md create mode 100644 .github/skills/install-scripts/SKILL.md diff --git a/.claude/skills/install-scripts/SKILL.md b/.claude/skills/install-scripts/SKILL.md new file mode 100644 index 00000000..5e0aa6a1 --- /dev/null +++ b/.claude/skills/install-scripts/SKILL.md @@ -0,0 +1,28 @@ +--- +name: install-scripts +description: Read before editing anything under install/ (production-filesystem/, ubuntu24/, etc.) or reviewing a PR that touches it. Covers uninstall safety, why check-*.sh scripts don't all exit the same way, and what's legacy vs. active. +--- + +# Install Scripts + +Conventions mined from real review comments on `install/*` PRs (#1214, #1230, #1235) — not derived from reading the scripts alone, so check current behavior before relying on any of these if the surrounding code has since changed. + +## Uninstall safety + +`install/*/uninstall.sh` must only remove NetAlertX's own files (`/app` and its own data) — never touch system packages it depends on (PHP, nginx, avahi, etc.) or other shared system components. A hardware/bare-metal install shares the host with other software; removing a system package on uninstall can break things NetAlertX didn't install. + +## `check-*.sh` scripts don't all exit the same way — by design + +`install/production-filesystem/services/scripts/check-*.sh` scripts are **not uniform** in whether a failure is fatal: +- Some are fail-fast on purpose: `check-first-run-config.sh` exits non-zero on a critical failure (can't create the config dir, can't copy default config) so the container doesn't start in a broken state. +- Some are warning-only on purpose: `check-ramdisk.sh` exits 0 even when it detects a real issue, letting the app start anyway with suboptimal config. + +Don't assume one check's exit-code convention applies to another — check what that specific script is actually guarding before suggesting a change to its exit behavior. + +## `/back` is legacy, not the active code path + +`back/cron_script.sh` (and the rest of `/back`) is legacy, kept only for compatibility with older/external components. The active, maintained version is `install/production-filesystem/services/scripts/cron_script.sh`. Changes to `/back` are out of scope for most PRs unless the PR is specifically about compatibility with whatever still depends on it. + +## Build-time vs. runtime, and generated files + +See the `devcontainer-configs`/`devcontainer-management` skills for the Dockerfile-generation and build-vs-runtime-file-availability rules — the same "source files aren't present at Docker build time" constraint applies to `install/production-filesystem/build/*` scripts, not just the devcontainer image. diff --git a/.claude/skills/pr-analysis/SKILL.md b/.claude/skills/pr-analysis/SKILL.md index 657c30d0..03d1a4be 100644 --- a/.claude/skills/pr-analysis/SKILL.md +++ b/.claude/skills/pr-analysis/SKILL.md @@ -5,6 +5,18 @@ description: How to analyze and respond to GitHub PR review comments in NetAlert # PR Analysis +## Standing Rule: A Repeated Comment Becomes a Skill Update + +jokob (the maintainer) has asked for this explicitly on real PRs, not just implied it: +- PR #1739 (test-mocking/MAC-case feedback repeated across two PRs): "will those skill prevent the issues I had to comment on for you to fix in the last 2 PRs? ... will you next time follow these based on available guidelines and skills? if not update those to make this explicit" +- PR #1744 (wrong `mylog()` level): "change 'warn' to 'none' and update skills so this doesn't happen again" + +So: if a review comment corrects something a skill *should* already cover, don't just fix that one instance — update the relevant skill as part of addressing the comment, same PR, same turn. If no skill covers it yet, that's the signal to create one. Check the skill first, though — sometimes it already covers the point and just wasn't consulted (e.g. `logging-standards` already lists the valid `mylog()` levels; the PR #1744 fix was in *applying* it, not in the skill being incomplete). + +## Code Style (from real review history) + +- **Prefer explicit, readable logic over compact-but-opaque expressions.** Real feedback on a `max()`-based one-liner: "make it easier to read, using max() obfuscates logic." When a built-in/clever expression saves a line or two but a reader has to reverse-engineer *why* it produces the right answer, write it out as plain conditional logic instead. + ## Before Writing Any Test Code — Non-Negotiable Checklist Run through this before creating or editing any file under `test/`: diff --git a/.gemini/skills/install-scripts/SKILL.md b/.gemini/skills/install-scripts/SKILL.md new file mode 100644 index 00000000..5e0aa6a1 --- /dev/null +++ b/.gemini/skills/install-scripts/SKILL.md @@ -0,0 +1,28 @@ +--- +name: install-scripts +description: Read before editing anything under install/ (production-filesystem/, ubuntu24/, etc.) or reviewing a PR that touches it. Covers uninstall safety, why check-*.sh scripts don't all exit the same way, and what's legacy vs. active. +--- + +# Install Scripts + +Conventions mined from real review comments on `install/*` PRs (#1214, #1230, #1235) — not derived from reading the scripts alone, so check current behavior before relying on any of these if the surrounding code has since changed. + +## Uninstall safety + +`install/*/uninstall.sh` must only remove NetAlertX's own files (`/app` and its own data) — never touch system packages it depends on (PHP, nginx, avahi, etc.) or other shared system components. A hardware/bare-metal install shares the host with other software; removing a system package on uninstall can break things NetAlertX didn't install. + +## `check-*.sh` scripts don't all exit the same way — by design + +`install/production-filesystem/services/scripts/check-*.sh` scripts are **not uniform** in whether a failure is fatal: +- Some are fail-fast on purpose: `check-first-run-config.sh` exits non-zero on a critical failure (can't create the config dir, can't copy default config) so the container doesn't start in a broken state. +- Some are warning-only on purpose: `check-ramdisk.sh` exits 0 even when it detects a real issue, letting the app start anyway with suboptimal config. + +Don't assume one check's exit-code convention applies to another — check what that specific script is actually guarding before suggesting a change to its exit behavior. + +## `/back` is legacy, not the active code path + +`back/cron_script.sh` (and the rest of `/back`) is legacy, kept only for compatibility with older/external components. The active, maintained version is `install/production-filesystem/services/scripts/cron_script.sh`. Changes to `/back` are out of scope for most PRs unless the PR is specifically about compatibility with whatever still depends on it. + +## Build-time vs. runtime, and generated files + +See the `devcontainer-configs`/`devcontainer-management` skills for the Dockerfile-generation and build-vs-runtime-file-availability rules — the same "source files aren't present at Docker build time" constraint applies to `install/production-filesystem/build/*` scripts, not just the devcontainer image. diff --git a/.gemini/skills/pr-analysis/SKILL.md b/.gemini/skills/pr-analysis/SKILL.md index ea12b2cc..e82c02b5 100644 --- a/.gemini/skills/pr-analysis/SKILL.md +++ b/.gemini/skills/pr-analysis/SKILL.md @@ -5,6 +5,18 @@ description: How to analyze and respond to GitHub PR review comments in NetAlert # PR Analysis +## Standing Rule: A Repeated Comment Becomes a Skill Update + +jokob (the maintainer) has asked for this explicitly on real PRs, not just implied it: +- PR #1739 (test-mocking/MAC-case feedback repeated across two PRs): "will those skill prevent the issues I had to comment on for you to fix in the last 2 PRs? ... will you next time follow these based on available guidelines and skills? if not update those to make this explicit" +- PR #1744 (wrong `mylog()` level): "change 'warn' to 'none' and update skills so this doesn't happen again" + +So: if a review comment corrects something a skill *should* already cover, don't just fix that one instance — update the relevant skill as part of addressing the comment, same PR, same turn. If no skill covers it yet, that's the signal to create one. Check the skill first, though — sometimes it already covers the point and just wasn't consulted (e.g. `logging-standards` already lists the valid `mylog()` levels; the PR #1744 fix was in *applying* it, not in the skill being incomplete). + +## Code Style (from real review history) + +- **Prefer explicit, readable logic over compact-but-opaque expressions.** Real feedback on a `max()`-based one-liner: "make it easier to read, using max() obfuscates logic." When a built-in/clever expression saves a line or two but a reader has to reverse-engineer *why* it produces the right answer, write it out as plain conditional logic instead. + ## Before Writing Any Test Code — Non-Negotiable Checklist Run through this before creating or editing any file under `test/`: diff --git a/.gemini/skills/skills-index/SKILL.md b/.gemini/skills/skills-index/SKILL.md index 56e03998..dc3f0eb5 100644 --- a/.gemini/skills/skills-index/SKILL.md +++ b/.gemini/skills/skills-index/SKILL.md @@ -25,8 +25,9 @@ Skills with the same purpose exist in more than one, sometimes under different n | Project navigation | `project-navigation` | `project-navigation` | — | Copilot version has full path tables and env vars; Gemini version is a brief reference | | Plugin dev | `plugin-development` | `plugin-run-development` | `plugin-development` | All three cover data contract, phases, formats, the `RUN_TIMEOUT` kill-timer gotcha (`timeoutMultiplier`/`per_item_timeout()`), and a pre-PR pointer to the Conventions Checklist in `docs/PLUGINS_DEV.md` | | Plugin README docs | `plugin-readme` | `plugin-readme` | `plugin-readme` | All three cover README structure, the "don't re-document settings" rule, the `docs.netalertx.com` cross-linking convention, and common defects (template leftovers, copy-paste errors) found during a full-repo audit | -| Devcontainer | `devcontainer-management` | `devcontainer-services` + `devcontainer-setup` + `devcontainer-configs` | — | Gemini combines into one (uses `docker exec`); Copilot splits into 3 focused skills | -| PR review | `pr-analysis` | `pr-analysis` | `pr-analysis` | How to classify and respond to PR comments; pre-flight skill loading checklist | +| Devcontainer | `devcontainer-management` | `devcontainer-services` + `devcontainer-setup` + `devcontainer-configs` | — | Gemini combines into one (uses `docker exec`); Copilot splits into 3 focused skills. `devcontainer-configs` also covers Dockerfile generation (root `Dockerfile` → `generate-dockerfile.sh` → `.devcontainer/Dockerfile`, source files not present at build time) - mined from real review comments on PR #1184/#1230. | +| Install scripts | `install-scripts` | `install-scripts` | `install-scripts` | `install/*` conventions mined from real review comments (#1214/#1230/#1235): uninstall must only remove NetAlertX's own files, never system packages; `check-*.sh` scripts intentionally differ on fail-fast vs. warning-only; `/back` is legacy, not the active code path. | +| PR review | `pr-analysis` | `pr-analysis` | `pr-analysis` | How to classify and respond to PR comments; pre-flight skill loading checklist; the standing rule that a repeated review comment becomes a skill update (sourced from jokob's own real PR comments, #1739/#1744); a readability-over-cleverness code-style note from the same source. | | Logging | `logging-standards` | `logging-standards` | — | `mylog` levels, message format, what not to log | | Scan pipeline internals | `scan-pipeline` | `scan-pipeline` | `scan-pipeline` | `process_scan()` call order and why it's load-bearing, `CurrentScan`/`Events`/`Sessions`/`DevicesView` relationships, how a session actually closes (no `close_session()` exists), and the `FIELD_SPECS` field-write authority mechanism. Complements `database-patterns` (Devices write-path/`*Source` attribution) rather than duplicating it. | | Database patterns | `database-patterns` | `database-patterns` | `database-patterns` | Devices table write-path inventory, the `FIELD_SOURCE_MAP`/`*Source` attribution system in `server/db/authoritative_handler.py`, SQLite trigger vs. Python-hook tradeoffs, and event-sourced vs. snapshot audit logging. | diff --git a/.github/skills/devcontainer-configs/SKILL.md b/.github/skills/devcontainer-configs/SKILL.md index bec8e250..196f69ad 100644 --- a/.github/skills/devcontainer-configs/SKILL.md +++ b/.github/skills/devcontainer-configs/SKILL.md @@ -26,3 +26,11 @@ Combines and merges template configurations into the final config used by VS Cod ## Note This affects only the devcontainer configuration. It has no bearing on the production or test Docker image. + +## Dockerfile Generation (separate from the config generation above) + +`.devcontainer/Dockerfile` is itself a **generated file** — `generate-dockerfile.sh` combines the root `Dockerfile` with `.devcontainer/resources/devcontainer-Dockerfile`. Never review or edit `.devcontainer/Dockerfile` directly; dependency/build-step changes belong in the root `Dockerfile`, regenerate after. + +Two build-time gotchas that have caused real review confusion: +- **Source repo files are not available during the Docker build phase** — they're mounted into the container after it starts. A `COPY` referencing a source file will fail at build time; config that depends on repo content has to be handled at runtime (a setup script), not a build step. +- Under modern BuildKit, `COPY` with a glob pattern that matches nothing (e.g. `.[V]ERSION`) succeeds silently — copies nothing, doesn't fail the build. Don't assume a missing optional file will surface as a build error. diff --git a/.github/skills/install-scripts/SKILL.md b/.github/skills/install-scripts/SKILL.md new file mode 100644 index 00000000..3782ebca --- /dev/null +++ b/.github/skills/install-scripts/SKILL.md @@ -0,0 +1,28 @@ +--- +name: netalertx-install-scripts +description: Read before editing anything under install/ (production-filesystem/, ubuntu24/, etc.) or reviewing a PR that touches it. Covers uninstall safety, why check-*.sh scripts don't all exit the same way, and what's legacy vs. active. +--- + +# Install Scripts + +Conventions mined from real review comments on `install/*` PRs (#1214, #1230, #1235) — not derived from reading the scripts alone, so check current behavior before relying on any of these if the surrounding code has since changed. + +## Uninstall safety + +`install/*/uninstall.sh` must only remove NetAlertX's own files (`/app` and its own data) — never touch system packages it depends on (PHP, nginx, avahi, etc.) or other shared system components. A hardware/bare-metal install shares the host with other software; removing a system package on uninstall can break things NetAlertX didn't install. + +## `check-*.sh` scripts don't all exit the same way — by design + +`install/production-filesystem/services/scripts/check-*.sh` scripts are **not uniform** in whether a failure is fatal: +- Some are fail-fast on purpose: `check-first-run-config.sh` exits non-zero on a critical failure (can't create the config dir, can't copy default config) so the container doesn't start in a broken state. +- Some are warning-only on purpose: `check-ramdisk.sh` exits 0 even when it detects a real issue, letting the app start anyway with suboptimal config. + +Don't assume one check's exit-code convention applies to another — check what that specific script is actually guarding before suggesting a change to its exit behavior. + +## `/back` is legacy, not the active code path + +`back/cron_script.sh` (and the rest of `/back`) is legacy, kept only for compatibility with older/external components. The active, maintained version is `install/production-filesystem/services/scripts/cron_script.sh`. Changes to `/back` are out of scope for most PRs unless the PR is specifically about compatibility with whatever still depends on it. + +## Build-time vs. runtime, and generated files + +See the `devcontainer-configs`/`devcontainer-management` skills for the Dockerfile-generation and build-vs-runtime-file-availability rules — the same "source files aren't present at Docker build time" constraint applies to `install/production-filesystem/build/*` scripts, not just the devcontainer image. diff --git a/.github/skills/pr-analysis/SKILL.md b/.github/skills/pr-analysis/SKILL.md index 38487210..776ba6dd 100644 --- a/.github/skills/pr-analysis/SKILL.md +++ b/.github/skills/pr-analysis/SKILL.md @@ -5,6 +5,18 @@ description: How to analyze and respond to GitHub PR review comments in NetAlert # PR Analysis +## Standing Rule: A Repeated Comment Becomes a Skill Update + +jokob (the maintainer) has asked for this explicitly on real PRs, not just implied it: +- PR #1739 (test-mocking/MAC-case feedback repeated across two PRs): "will those skill prevent the issues I had to comment on for you to fix in the last 2 PRs? ... will you next time follow these based on available guidelines and skills? if not update those to make this explicit" +- PR #1744 (wrong `mylog()` level): "change 'warn' to 'none' and update skills so this doesn't happen again" + +So: if a review comment corrects something a skill *should* already cover, don't just fix that one instance — update the relevant skill as part of addressing the comment, same PR, same turn. If no skill covers it yet, that's the signal to create one. Check the skill first, though — sometimes it already covers the point and just wasn't consulted (e.g. `logging-standards` already lists the valid `mylog()` levels; the PR #1744 fix was in *applying* it, not in the skill being incomplete). + +## Code Style (from real review history) + +- **Prefer explicit, readable logic over compact-but-opaque expressions.** Real feedback on a `max()`-based one-liner: "make it easier to read, using max() obfuscates logic." When a built-in/clever expression saves a line or two but a reader has to reverse-engineer *why* it produces the right answer, write it out as plain conditional logic instead. + ## Before Writing Any Test Code — Non-Negotiable Checklist Run through this before creating or editing any file under `test/`: diff --git a/.github/skills/skills-overview/SKILL.md b/.github/skills/skills-overview/SKILL.md index 40607658..200e9a86 100644 --- a/.github/skills/skills-overview/SKILL.md +++ b/.github/skills/skills-overview/SKILL.md @@ -25,8 +25,9 @@ Skills with the same purpose exist in more than one, sometimes under different n | Project navigation | `project-navigation` | `project-navigation` | — | Copilot version has full path tables and env vars; Gemini version is a brief reference | | Plugin dev | `plugin-run-development` | `plugin-development` | `plugin-development` | All three cover data contract, phases, formats, the `RUN_TIMEOUT` kill-timer gotcha (`timeoutMultiplier`/`per_item_timeout()`), and a pre-PR pointer to the Conventions Checklist in `docs/PLUGINS_DEV.md` | | Plugin README docs | `plugin-readme` | `plugin-readme` | `plugin-readme` | All three cover README structure, the "don't re-document settings" rule, the `docs.netalertx.com` cross-linking convention, and common defects (template leftovers, copy-paste errors) found during a full-repo audit | -| Devcontainer | `devcontainer-services` + `devcontainer-setup` + `devcontainer-configs` | `devcontainer-management` | — | Copilot splits into 3 focused skills; Gemini combines into one (uses `docker exec`) | -| PR review | `pr-analysis` | `pr-analysis` | `pr-analysis` | How to classify and respond to PR comments; pre-flight skill loading checklist | +| Devcontainer | `devcontainer-services` + `devcontainer-setup` + `devcontainer-configs` | `devcontainer-management` | — | Copilot splits into 3 focused skills; Gemini combines into one (uses `docker exec`). `devcontainer-configs` also covers Dockerfile generation (root `Dockerfile` → `generate-dockerfile.sh` → `.devcontainer/Dockerfile`, source files not present at build time) - mined from real review comments on PR #1184/#1230. | +| Install scripts | `install-scripts` | `install-scripts` | `install-scripts` | `install/*` conventions mined from real review comments (#1214/#1230/#1235): uninstall must only remove NetAlertX's own files, never system packages; `check-*.sh` scripts intentionally differ on fail-fast vs. warning-only; `/back` is legacy, not the active code path. | +| PR review | `pr-analysis` | `pr-analysis` | `pr-analysis` | How to classify and respond to PR comments; pre-flight skill loading checklist; the standing rule that a repeated review comment becomes a skill update (sourced from jokob's own real PR comments, #1739/#1744); a readability-over-cleverness code-style note from the same source. | | Logging | `logging-standards` | `logging-standards` | — | `mylog` levels, message format, what not to log | | Scan pipeline internals | `scan-pipeline` | `scan-pipeline` | `scan-pipeline` | `process_scan()` call order and why it's load-bearing, `CurrentScan`/`Events`/`Sessions`/`DevicesView` relationships, how a session actually closes (no `close_session()` exists), and the `FIELD_SPECS` field-write authority mechanism. Complements `database-patterns` (Devices write-path/`*Source` attribution) rather than duplicating it. | | Database patterns | `database-patterns` | `database-patterns` | `database-patterns` | Devices table write-path inventory, the `FIELD_SOURCE_MAP`/`*Source` attribution system in `server/db/authoritative_handler.py`, SQLite trigger vs. Python-hook tradeoffs, and event-sourced vs. snapshot audit logging. | diff --git a/scripts/check_skill_pairs.py b/scripts/check_skill_pairs.py index 6cf2ef98..f71aa974 100644 --- a/scripts/check_skill_pairs.py +++ b/scripts/check_skill_pairs.py @@ -34,6 +34,7 @@ GROUPS = [ [".gemini/skills/ux-design-patterns/SKILL.md", ".github/skills/ux-design-patterns/SKILL.md", ".claude/skills/ux-design-patterns/SKILL.md"], [".gemini/skills/skill-hygiene/SKILL.md", ".github/skills/skill-hygiene/SKILL.md", ".claude/skills/skill-hygiene/SKILL.md"], [".gemini/skills/plugin-review/SKILL.md", ".github/skills/plugin-review/SKILL.md", ".claude/skills/plugin-review/SKILL.md"], + [".gemini/skills/install-scripts/SKILL.md", ".github/skills/install-scripts/SKILL.md", ".claude/skills/install-scripts/SKILL.md"], [".gemini/skills/settings/SKILL.md", ".github/skills/settings-management/SKILL.md"], [".gemini/skills/mcp-activation/SKILL.md", ".github/skills/mcp-activation/SKILL.md"], [".gemini/skills/project-navigation/SKILL.md", ".github/skills/project-navigation/SKILL.md"], diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index a4ac30b7..b83d5277 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -912,7 +912,7 @@ class CreateNotificationRequest(BaseModel): content: str = Field( ..., min_length=1, - max_length=1024, + max_length=4096, description="Notification content" ) level: NOTIFICATION_LEVELS = Field( @@ -920,6 +920,18 @@ class CreateNotificationRequest(BaseModel): description="Notification severity level" ) + @field_validator("content", mode="before") + @classmethod + def truncate_content(cls, v): + """Truncate instead of rejecting - content is often a generated + summary (e.g. a bulk multi-edit's affected MAC list) with no natural + length cap, and the notification store is a JSON file with no + underlying column limit that 4096 corresponds to. Losing the whole + write on overlength input would be worse than a truncated one.""" + if isinstance(v, str) and len(v) > 4096: + return v[:4093] + "..." + return v + # ============================================================================= # SYNC SCHEMAS diff --git a/test/api_endpoints/test_messaging_in_app_endpoints.py b/test/api_endpoints/test_messaging_in_app_endpoints.py index b41daac3..8a037045 100644 --- a/test/api_endpoints/test_messaging_in_app_endpoints.py +++ b/test/api_endpoints/test_messaging_in_app_endpoints.py @@ -107,6 +107,46 @@ def test_delete_single_notification(client, api_token, notification_guid): assert resp.json.get("success") is True +def test_write_notification_truncates_overlong_content(client, api_token): + """A notification whose content exceeds 4096 chars (e.g. a bulk multi-edit's + generated MAC-list summary, selecting hundreds of devices) must be + truncated, not rejected outright - silently dropping the whole + notification loses the audit trail of what the bulk action did.""" + overlong = "[Multi edit] Executed \"update\" matching \"" + ",".join( + f"{i:04x}:{i:04x}:{i:04x}" for i in range(300) + ) + "\"" + assert len(overlong) > 4096 + + resp = client.post( + "/messaging/in-app/write", + json={"content": overlong, "level": "info"}, + headers=auth_headers(api_token) + ) + assert resp.status_code == 200 + assert resp.json.get("success") is True + + resp = client.get("/messaging/in-app/unread", headers=auth_headers(api_token)) + stored = next((n["content"] for n in resp.json if n["content"].startswith("[Multi edit]")), None) + assert stored is not None + assert len(stored) == 4096 + assert stored.endswith("...") + assert stored.startswith(overlong[:100]) + + +def test_write_notification_at_exact_limit_unchanged(client, api_token): + exactly_4096 = "x" * 4096 + resp = client.post( + "/messaging/in-app/write", + json={"content": exactly_4096, "level": "info"}, + headers=auth_headers(api_token) + ) + assert resp.status_code == 200 + + resp = client.get("/messaging/in-app/unread", headers=auth_headers(api_token)) + stored = next((n["content"] for n in resp.json if n["content"].startswith("xxx")), None) + assert stored == exactly_4096 # untouched - no trailing "..." + + def test_delete_all_notifications(client, api_token, random_content): # Add a notification first client.post("/messaging/in-app/write", json={"content": random_content}, headers=auth_headers(api_token))