Przeglądaj źródła

Cleanup pass: drop push archaeology and stale test expectations

The final normalisation pass over the web push surface:

- Remove the dead base64url_encode helper (subscription validation
  only ever needed decode).
- Drop plan-11/task-E/goal-47 archaeology from doc comments across
  PushService, PushApi, PushActions, Entities, main, the entrypoints,
  home.html, harness.py and the push bundle, so the code reads as if
  Spry's push had always been there. The migrations keep their
  historical notes (M0012 IS history: the move from our hand-rolled
  table into Spry's store).
- M0011's user_id note no longer cites the push_subscriptions posture
  (that table no longer exists after M0012).
- pages.py's migration assertions now expect clanker-saddle serials 1
  through 12 (M0012 joined the chain; verified on a fresh boot: each
  serial applies exactly once, M0012 last after the spry namespace via
  its dependencies edge).
- .gitignore the e2e scratch workdir (build/).
saddle-ClankerSaddle 11 godzin temu
rodzic
commit
4aefa8300f

+ 3 - 0
.gitignore

@@ -8,3 +8,6 @@ compile_commands.json
 # Python bytecode
 test/__pycache__/
 __pycache__/
+
+# E2E scratch workdirs (WEBSTACK_WORK=<repo>/build/ws)
+build/

+ 2 - 2
src/Api/PushApi.vala

@@ -6,8 +6,8 @@ using ClankerSaddle.Services;
 namespace ClankerSaddle.Api {
 
     /**
-     * `GET /sw.js` (plan 11 task E) — the Web Push service worker, a
-     * static asset served at the origin root so its scope covers every
+     * `GET /sw.js` — the Web Push service worker, a static asset
+     * served at the origin root so its scope covers every
      * page. The push handler shows the notification the app built
      * ({title, body, url}); `notificationclick` focuses an open window
      * on the task page or opens one. Deliberately unauthenticated: the

+ 2 - 2
src/Data/Entities.vala

@@ -594,8 +594,8 @@ namespace ClankerSaddle.Data {
     }
 
     /**
-     * A user's per-task "Notify me" opt-in (plan 11 task E shape): the
-     * task's first start, failure and completion notify the opted-in
+     * A user's per-task "Notify me" opt-in: the task's first start,
+     * failure and completion notify the opted-in
      * user; block events notify all push-subscribed users globally.
      */
     public class TaskNotifyPref : Object {

+ 1 - 2
src/Data/Migrations/M0011_UserPreferences.vala

@@ -12,8 +12,7 @@
          * need no further migrations.
          *
          * `user_id` references the Spry users table BY VALUE (no FK —
-         * the auth schema owns that table, the same posture as
-         * `push_subscriptions`).
+         * the auth schema owns that table).
          *
          * IDEMPOTENCE: same contract as M0002–M0010 — up() runs before
          * the apply transaction opens (the builder only queues), so it

+ 2 - 2
src/Data/Migrations/M0012_ProviderPushSubscriptions.vala

@@ -4,8 +4,8 @@
     namespace ClankerSaddle.Data.Migrations {
 
         /**
-         * Moves Web Push subscriptions from our hand-rolled
-         * `push_subscriptions` table (plan 11 task E) into Spry's
+         * Moves Web Push subscriptions from our former hand-rolled
+         * `push_subscriptions` table into Spry's
          * `spry_push_subscriptions` store (goal 47: the app adopted
          * `Spry.Push` for storage, crypto and delivery — our own RFC
          * 8291/8292 implementation never worked and is gone).

+ 1 - 1
src/Services/AllocationService.vala

@@ -588,7 +588,7 @@ namespace ClankerSaddle.Services {
 
             yield saddles.mark_task_comments_delivered(task.id);
             yield pulse.bump(task.id, "task dispatched");
-            // Web Push (plan 11 task E): the FIRST dispatch of a task
+            // Web Push: the FIRST dispatch of a task
             // notifies its opted-in users — fired from the same place
             // the dispatch pulse bumps (single funnel).
             if (claim.first_start) {

+ 2 - 20
src/Services/PushService.vala

@@ -39,7 +39,7 @@ namespace ClankerSaddle.Services {
     }
 
     /**
-     * The app's Web Push layer over `Spry.Push` (goal 47): payloads,
+     * The app's Web Push layer over `Spry.Push`: payloads,
      * audiences and notify prefs are OUR domain — Spry owns the store
      * (`spry_push_subscriptions`), the RFC 8291/8292 crypto and the
      * delivery POST. This class never touches a key or a socket.
@@ -447,8 +447,7 @@ namespace ClankerSaddle.Services {
                 var rows = yield scope.resolve<Spry.Push.PushService>()
                     .subscriptions_for(user_id);
                 // Spry's store order is unspecified: sort by creation,
-                // then id, newest first (the same order the old store
-                // gave).
+                // then id, newest first.
                 var by_recency = new GLib.List<Spry.Push.PushSubscriptionEntity>();
                 foreach (var row in rows) {
                     by_recency.append(row);
@@ -530,23 +529,6 @@ namespace ClankerSaddle.Services {
         // Primitives
         // ------------------------------------------------------------------
 
-        /** Standard base64 -> base64url (unpadded). */
-        public static string base64url_encode(uint8[] data) {
-            string encoded = Base64.encode(data);
-            var builder = new StringBuilder();
-            for (var index = 0; index < encoded.length; index++) {
-                char character = encoded[index];
-                if (character == '+') {
-                    builder.append_c('-');
-                } else if (character == '/') {
-                    builder.append_c('_');
-                } else if (character != '=') {
-                    builder.append_c(character);
-                }
-            }
-            return builder.str;
-        }
-
         /** base64url (padded or not) -> bytes; junk decodes empty. */
         public static uint8[] base64url_decode(string text) {
             var normalised = new StringBuilder();

+ 3 - 3
src/Services/TaskService.vala

@@ -895,7 +895,7 @@ namespace ClankerSaddle.Services {
             raised_payload.set_string_member("text", @"$(block.block_type) raised: $(block.question)");
             yield events.append(EVENT_STATE_CHANGE, EVENT_ACTOR_SYSTEM, EVENT_SCOPE_TASK,
                 task_id, raised_payload);
-            // Web Push (plan 11 task E): every raised block notifies all
+            // Web Push: every raised block notifies all
             // push-subscribed users globally — the goal agent's mirrored
             // blocks stay quiet (the goal-level block owns the
             // notification with the goal's title and URL).
@@ -1287,7 +1287,7 @@ namespace ClankerSaddle.Services {
 
             yield release_task(task_id, outcome, false);
             yield pulse.bump(task_id, "task completed");
-            // Web Push (plan 11 task E): completion notifies the users
+            // Web Push: completion notifies the users
             // holding a "Notify me" pref for this task.
             push.notify_opted_in(task_id, PushService.lifecycle_notification(
                 ((!)task).title, task_id, "completed", summary));
@@ -1362,7 +1362,7 @@ namespace ClankerSaddle.Services {
 
             yield release_task(task_id, OUTCOME_FAILED, false);
             yield pulse.bump(task_id, "task failed");
-            // Web Push (plan 11 task E): an agent-reported failure
+            // Web Push: an agent-reported failure
             // notifies the opted-in users (a human cancellation does
             // not — the cancellator already knows).
             push.notify_opted_in(task_id, PushService.lifecycle_notification(

+ 3 - 3
src/actions/PushActions.vala

@@ -8,7 +8,7 @@ namespace ClankerSaddle {
 
     /**
      * Stores the browser's Web Push subscription for the signed-in
-     * user (plan 11 task E): `endpoint`, `p256dh` and `auth` arrive
+     * user: `endpoint`, `p256dh` and `auth` arrive
      * from the dashboard bell's subscribe flow
      * (`pushManager.subscribe` + the VAPID key from the page state).
      * Validated per {@link PushService.validate_subscription}; the
@@ -78,8 +78,8 @@ namespace ClankerSaddle {
     }
 
     /**
-     * Flips the signed-in user's per-task "Notify me" pref (plan 11
-     * task E): opted-in users receive the task's first start, failure
+     * Flips the signed-in user's per-task "Notify me" pref: opted-in
+     * users receive the task's first start, failure
      * and completion on top of the global block events every
      * push-subscribed user gets.
      */

+ 1 - 1
src/entrypoints/HomeEntrypoint.vala

@@ -419,7 +419,7 @@ namespace ClankerSaddle {
             // per-instance and the endpoint objects do the same.
             var canon = new Services.Canon();
             pub.canon_texts = canon.texts().length;
-            // Web Push bell state (plan 11 task E; per-browser since
+            // Web Push bell state (per-browser since
             // the user-global flag lied to second browsers): the VAPID
             // key the browser needs (Spry's sender, `spry keys`-owned —
             // "" when unkeyed), whether this user subscribed

+ 1 - 1
src/entrypoints/TaskDetailEntrypoint.vala

@@ -268,7 +268,7 @@ namespace ClankerSaddle {
             pub.refresh = actions.author_private<RefreshTaskDataAction, TaskActionPrivate>(sealed, "GET");
             pub.attach_rule = actions.author_private<AttachTaskRuleAction, TaskActionPrivate>(sealed);
             pub.attach_capability = actions.author_private<AttachTaskCapabilityAction, TaskActionPrivate>(sealed);
-            // Web Push notify-me state (plan 11 task E): the signed-in
+            // Web Push notify-me state: the signed-in
             // user's per-task opt-in for start/failure/completion.
             if (viewer_id > 0) {
                 pub.notify_me = yield push.has_notify_pref(db, viewer_id, task.id);

+ 1 - 1
src/main.vala

@@ -71,7 +71,7 @@ int main(string[] args) {
         db.add_entity<Capability>(Capability.entity_mapping);
         db.add_entity<SaddleCapability>(SaddleCapability.entity_mapping);
         db.add_entity<TaskCapability>(TaskCapability.entity_mapping);
-        // Web Push storage/delivery is Spry's (goal 47): SpryModule
+        // Web Push storage/delivery is Spry's: SpryModule
         // registers the spry_push_subscriptions entity, migrations and
         // the singleton PushSender. The app's registration below adds
         // OUR push entity to the same OrmSession so raw queries see it.

+ 3 - 3
src/pages/home.html

@@ -7,8 +7,8 @@
     // Live dashboard: the pulse subscription (see the entrypoint)
     // re-runs the sealed refresh action whenever task or saddle state
     // changes, so progress, blocks and review requests stream in.
-    // The notification bell drives the Web Push subscribe flow (plan
-    // 11 task E) - the shared engine lives in app.js
+    // The notification bell drives the Web Push subscribe flow - the
+    // shared engine lives in app.js
     // (window.initPushBell, goal 41 task 3: the dashboard's compact
     // rail strip and /profile's settings card run the same code).
     if (window.statum) {
@@ -244,7 +244,7 @@
                         <p><a href="/saddles">Manage saddles</a></p>
                     </section>
 
-                    <!-- The notification bell (plan 11 task E; per-browser
+                    <!-- The notification bell (per-browser
                          since the user-global flag lied to second
                          browsers): THIS browser's own subscription decides
                          Enable vs Turn off (the JS fingerprints the

+ 6 - 5
test/bundles/pages.py

@@ -948,14 +948,15 @@ def run(ctx):
         "select name from sqlite_master"
         " where type = 'index' and tbl_name = 'events'").fetchall()}
     conn.close()
-    check([s[0] for s in serials] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
-          "the clanker-saddle namespace carries exactly serials 1 through 11"
+    check([s[0] for s in serials] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+          "the clanker-saddle namespace carries exactly serials 1 through 12"
           " (a fresh boot ran M0001 then the M0002 no-op then M0003,"
           " M0004 understandings, M0005 discoveries, M0006 advisor,"
           " M0007 the parking guard, M0008 answer-wake, M0009 the"
-          " turn-cap park, M0010 the model config and M0011 the user"
-          " preferences)")
-    check(len(serials) == 11,
+          " turn-cap park, M0010 the model config, M0011 the user"
+          " preferences and M0012 the move of push subscriptions into"
+          " Spry's store)")
+    check(len(serials) == 12,
           "no duplicate migration rows (each guarded up() applied once)")
     check({"idx_events_scope", "idx_events_terminal"} <= event_indexes,
           "M0003's event reader indexes exist (fresh boot)")

+ 9 - 9
test/bundles/push.py

@@ -1,4 +1,4 @@
-"""The push bundle (goal 47: rewritten for Spry.Push semantics; see
+"""The push bundle: Web Push over `Spry.Push` semantics (see
 test/harness.py for the shared machinery and the runner)."""
 from harness import *  # noqa: F401,F403 — the shared wire helpers
 
@@ -26,13 +26,13 @@ def run(ctx):
         finally:
             conn.close()
 
-    print("== task E: service worker asset ==")
+    print("== service worker asset ==")
     status, sw = http("GET", "/sw.js", timeout=15)
     check(status == 200 and "addEventListener('push'" in sw
           and "notificationclick" in sw and "clients.openWindow" in sw,
           "the service worker serves at the origin root with push + click routing")
 
-    print("== goal 47: VAPID keys from the \"spry\" config section ==")
+    print("== VAPID keys from the \"spry\" config section ==")
     # Spry owns the keys: `spry keys` writes the "spry" section the app
     # reads — the app no longer generates or persists key material.
     config = json.load(open(os.path.join(WORK, "web-config.json")))
@@ -53,7 +53,7 @@ def run(ctx):
     check(dash.get("push_subscribed") is False and bool(dash.get("push_subscribe")),
           "the bell starts unsubscribed and carries the subscribe action")
 
-    print("== task E: subscription CRUD + auth validation ==")
+    print("== subscription CRUD + auth validation ==")
     browser = ec.generate_private_key(ec.SECP256R1())
     browser_public = browser.public_key().public_bytes(
         Encoding.X962, PublicFormat.UncompressedPoint)
@@ -101,7 +101,7 @@ def run(ctx):
     check(len(rows) == 1 and rows[0][0] == b64url(rotated_auth),
           "re-subscribing an endpoint replaces its keys in place")
 
-    print("== task E: notify-me pref persistence ==")
+    print("== notify-me pref persistence ==")
     act(auth, slot_public(auth, "/tasks", "tasks")["create"],
         {"project_id": "1", "title": "Push pref task", "priority": "5"})
     pref_task = task_id_of("Push pref task")
@@ -122,7 +122,7 @@ def run(ctx):
     check(task_state(auth, pref_task).get("notify_me") is False,
           "the task slot reflects the opt-out")
 
-    print("== task E: dispatch matrix — global blocks vs opted-in lifecycle ==")
+    print("== dispatch matrix — global blocks vs opted-in lifecycle ==")
     # Two "browsers" for admin (/main, /gone) and one for another user
     # (id 999, rows seeded directly — push targeting is per user).
     endpoint_gone = push.url("/gone")
@@ -278,7 +278,7 @@ def run(ctx):
     check(len(push.hits("/main")) == main_before2,
           "an agent failure never reaches users without the pref")
 
-    print("== goal 47: inline retry (Spry's bounded delivery) ==")
+    print("== inline retry (Spry's bounded delivery) ==")
     # Spry's sender retries INLINE: two attempts, 250ms apart, then
     # classifies. One transient 503 therefore lands on the second
     # attempt moments later — no dispatcher tick, no queue row.
@@ -306,7 +306,7 @@ def run(ctx):
     else:
         check(False, "the inline retry fixture did not land")
 
-    print("== goal 47: a hard-failing endpoint is RETRYABLE, never swept ==")
+    print("== a hard-failing endpoint is RETRYABLE, never swept ==")
     # RFC 8030 §7 semantics (Spry): a persistent 503 exhausts the inline
     # attempts and classifies RETRYABLE — the notification is NOT
     # delivered, but the subscription row SURVIVES (only 401/403/404/410
@@ -425,7 +425,7 @@ def run(ctx):
           "the goal success payload carries the goal title + click URL (%s)"
           % done_goal_payload)
 
-    print("== task E: unsubscribe removes the subscription ==")
+    print("== unsubscribe removes the subscription ==")
     dash = slot_public(auth, "/", "dashboard")
     check(dash.get("push_subscribed") is True,
           "the bell shows subscribed while endpoints exist")

+ 6 - 7
test/harness.py

@@ -100,7 +100,7 @@ Plan 11 task D+E (UI + notifications), the `push` shard:
   behind an expander, answered blocks behind an expander; queue screen:
   New Task expander above the filters; dashboard waiting-on-drafts
   block with links to where Enqueue lives (pages shard)
-- real Web Push over VAPID (goal 47: Spry.Push owns the plumbing — the
+- real Web Push over VAPID (Spry.Push owns the plumbing — the
   store over spry_push_subscriptions, the RFC 8291/8292 crypto and the
   delivery POST with inline 2-attempt/250ms retry; the app keeps the
   payloads, audiences, notify prefs and the bell UX): keys come from
@@ -163,7 +163,7 @@ import urllib.parse
 import urllib.request
 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
 
-# Web Push protocol verification (plan 11 task E): the test harness
+# Web Push protocol verification: the test harness
 # owns the browser-side keys, so it can do everything the push service
 # does — decrypt the RFC 8291 body and verify the ES256 VAPID JWT.
 import base64
@@ -1178,7 +1178,7 @@ def dump_app_stacks(pid):
 
 
 # =============================================================================
-# Fake push service + Web Push protocol helpers (plan 11 task E)
+# Fake push service + Web Push protocol helpers
 # =============================================================================
 
 def b64url(raw):
@@ -1193,7 +1193,7 @@ class FakePushService:
     """In-process Web Push endpoint server: every subscription the tests
     register points at a path here (a distinct "browser endpoint").
     Responses are scriptable per path — a fixed status (the 410-sweep
-    fixture) or N transient 500s (the inline-retry fixture; goal 47:
+    fixture) or N transient 500s (the inline-retry fixture:
     Spry's sender retries inline 2x/250ms, so N=1 means "lands on the
     second attempt")."""
 
@@ -1253,9 +1253,8 @@ class FakePushService:
 
 
 def decrypt_webpush(request, client_private_key, client_public, auth_secret):
-    """RFC 8291 decryption of one captured push (goal 47: Spry/Locksmith's
-    schedule — the old helper matched our deleted home-grown crypto and
-    could not open a standards-conformant envelope). Returns
+    """RFC 8291 decryption of one captured push (Spry/Locksmith's
+    schedule). Returns
     (record_size, payload dict) — the payload builder's exact output."""
     body = request["body"]
     salt = body[:16]