p='crates/spt/src/api/nowsignal.rs'
b=open(p,'rb').read(); crlf=b'\r\n' in b; s=b.decode().replace('\r\n','\n')
a='''    /// LAN_EXPOSED appears while the listener is up and is GONE after stop —'''
assert s.count(a)==1
new='''    fn moved(old: &str, new: &str, changelog: Option<&str>, notice: Option<&str>) -> spt_daemon::SubjectApply {
        spt_daemon::SubjectApply {
            old: old.into(),
            new: new.into(),
            // 2026-09-24T16:58:00Z — 09:58 at UTC-7.
            at_ms: 1_790_269_080_000,
            changelog: changelog.map(str::to_string),
            notice: notice.map(str::to_string),
        }
    }

    fn pdt() -> chrono::FixedOffset {
        chrono::FixedOffset::west_opt(7 * 3600).unwrap()
    }

    /// The pinned NEGATIVE CONTROL: every subject whose version did not move
    /// keeps today's one-line shape TO THE BYTE — pinned as literals, not as a
    /// re-derivation of the formatter under test — including every degrade
    /// arm: no record, a record that landed on a DIFFERENT version (a rollback
    /// or a stale move), an empty old version, and an old equal to the new.
    // [unit->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    #[test]
    fn an_unmoved_subject_keeps_its_row_to_the_byte() {
        let tz = pdt();
        let rows = [
            ("spt-core", "0.72.0", "spt-core 0.72.0"),
            ("harness adapter claude-spt", "0.41.3", "harness adapter claude-spt 0.41.3"),
            ("shell PACER-0 adapter PACER", "0.7.0", "shell PACER-0 adapter PACER 0.7.0"),
        ];
        for (label, version, today) in rows {
            assert_eq!(update_row(label, version, None, &tz), today);
            let stale = moved("0.1.0", "9.9.9", Some("http://x/changelog"), Some("note"));
            assert_eq!(update_row(label, version, Some(&stale), &tz), today, "record for another version");
            let no_old = moved("", version, Some("http://x/changelog"), None);
            assert_eq!(update_row(label, version, Some(&no_old), &tz), today, "no prior version");
            let same = moved(version, version, None, None);
            assert_eq!(update_row(label, version, Some(&same), &tz), today, "not a move");
        }
    }

    /// A MOVED subject: `UPDATED <subject> <old> → <new> at <HH:MMAM|PM
    /// YYYY-MM-DD>`, local 12-hour clock zero-padded, then the changelog, then
    /// the post-update notice folded onto the one line.
    // [unit->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    #[test]
    fn a_moved_subject_is_told_as_a_move_with_time_changelog_and_notice() {
        let tz = pdt();
        let m = moved(
            "0.41.2",
            "0.41.3",
            Some("http://localhost:5474/hfenduleam/a/claude-spt/changelog"),
            Some("Reload required:\n  run /reload-plugins."),
        );
        assert_eq!(
            update_row("harness adapter claude-spt", "0.41.3", Some(&m), &tz),
            "UPDATED harness adapter claude-spt 0.41.2 → 0.41.3 at 09:58AM 2026-09-24 — \
             changelog: http://localhost:5474/hfenduleam/a/claude-spt/changelog — \
             Reload required: run /reload-plugins."
        );
        // PM, and a different zone moves the wall clock (the time is LOCAL).
        let utc = chrono::FixedOffset::east_opt(0).unwrap();
        let core = moved("0.72.0", "0.73.0", Some("http://localhost:5474/n/docs/changelog.html"), None);
        assert_eq!(
            update_row("spt-core", "0.73.0", Some(&core), &utc),
            "UPDATED spt-core 0.72.0 → 0.73.0 at 04:58PM 2026-09-24 — \
             changelog: http://localhost:5474/n/docs/changelog.html"
        );
    }

    /// The NO-DECLARATION arm — the common case, not the edge: an adapter that
    /// declares no changelog ends its row after the applied-at time, with no
    /// dangling separator and no empty link.
    // [unit->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    #[test]
    fn a_subject_without_a_changelog_ends_after_the_time() {
        let tz = pdt();
        let m = moved("0.6.0", "0.7.0", None, None);
        assert_eq!(
            update_row("shell PACER-0 adapter PACER", "0.7.0", Some(&m), &tz),
            "UPDATED shell PACER-0 adapter PACER 0.6.0 → 0.7.0 at 09:58AM 2026-09-24"
        );
        let blank = moved("0.6.0", "0.7.0", Some("  "), Some(" \n "));
        assert_eq!(
            update_row("shell PACER-0 adapter PACER", "0.7.0", Some(&blank), &tz),
            "UPDATED shell PACER-0 adapter PACER 0.6.0 → 0.7.0 at 09:58AM 2026-09-24"
        );
    }

    /// Through the category: the apply record reshapes the row the seen-set
    /// already decided to tell, and NEVER tells one itself — the record stays
    /// on disk, yet the second poll is empty, because the seen-set remains the
    /// only event detector.
    // [unit->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    #[test]
    fn the_apply_record_reshapes_a_told_row_but_never_tells_one() {
        let _home = crate::testutil::isolated_home();
        let session = session_id("updates-divulge");
        clear_session(&session);
        let ctx = Ctx {
            adapter: None,
            manifest: None,
            install_dir: None,
        };
        let input = PollInput {
            id: "updates-divulge-test",
            session: &session,
            user_input: "",
            agent_output: "",
        };
        let running = env!("CARGO_PKG_VERSION");
        spt_daemon::ReleaseCache::open(&perch::spt_home().join("releases"))
            .record_subject_apply(
                spt_daemon::SUBJECT_CORE,
                &moved("0.0.1", running, Some("http://localhost:5474/n/docs/changelog.html"), None),
            )
            .unwrap();

        let mut seen = SeenSet::load(&session, Category::Updates);
        let first = gather_updates(&ctx, &input, &mut seen);
        seen.flush();
        let want = format!("UPDATED spt-core 0.0.1 → {running} at ");
        assert!(first.iter().any(|l| l.starts_with(&want)), "{first:?}");
        assert!(
            first.iter().any(|l| l.ends_with(" — changelog: http://localhost:5474/n/docs/changelog.html")),
            "{first:?}"
        );

        let mut seen = SeenSet::load(&session, Category::Updates);
        let second = gather_updates(&ctx, &input, &mut seen);
        seen.flush();
        assert!(second.is_empty(), "the record is not an event source: {second:?}");
        clear_session(&session);
    }

'''
s=s.replace(a,new+a)
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
