#!/usr/bin/env node // `$`-safety structural guard for hooks/scripts/state-updater.mjs (remediation S13). // // WHY: the S9→S12 "close the class, not the line" lesson recurred. S12 converted the // 5 section-append `String.replace` sites to replacement *functions* but left // `replaceField` — the scalar writer — on a replacement *string*, so an untrusted // `last_post_topic` carrying `$&`/`$1`/`` $` `` silently corrupted state, and the S12 // test asserted only the section entry (never the scalar), shipping the bug GREEN. // The class is "untrusted user content reaching ANY `String.replace` replacement // STRING": in a replacement string `$&` expands to the whole match, `` $` ``/`$'` to the // pre/suffix, `$$`→`$`, `$n`→group n — so a `$`-bearing value rewrites the field. A // replacement FUNCTION returns its string verbatim (no `$`-substitution), closing the // class by construction. // // This guard proves the PROPERTY behaviorally rather than grepping a syntactic proxy // (which cannot tell a replacement-position template literal from a RegExp-pattern one // across multi-line calls): it drives every exported state mutator with an adversarial // payload built from EVERY special replacement token, in every free-text AND date // field that reaches a `.replace()`, and asserts the payload survives VERBATIM. Two // structural backstops make it a CLASS guard, not a per-line test: // (1) COVERAGE-COMPLETENESS — every exported mutator (minus the documented I/O // wrapper) must appear in the battery. A NEW export added without `$`-coverage // fails here, so future code cannot reintroduce the class unguarded. // (2) NON-VACUITY SELF-TEST — a naive string-replace fed the same payload MUST // corrupt, and a function replacement MUST preserve it. A guard that cannot // observe the corruption it forbids certifies nothing, so it fails the suite // instead (mirrors Section 8/10/11 self-tests; the S7→S10 "proof run once by // hand, never committed" lesson applied to the `$`-axis). // // Zero dependencies (node:assert + the module under test). Invoked from // scripts/test-runner.sh Section 12; exit code mapped to pass/fail. import assert from "node:assert/strict"; import * as mod from "../hooks/scripts/state-updater.mjs"; // Adversarial payload: every JS replacement-string special token. If ANY of these is // interpreted (string replacement) instead of inserted literally (function // replacement), the payload will not survive verbatim. const TOKENS = ["$&", "$`", "$'", "$$", "$1", "$0"]; const PAYLOAD = `lead ${TOKENS.join(" ")} tail`; // Minimal fixtures (mirror hooks/scripts/__tests__/state-updater.test.mjs). SAMPLE // omits last_firsthour_date/last_outreach_date (additive-insert path); WITH_FH carries // last_firsthour_date (the replaceField scalar path); TEMPLATE ships the two append // sections pre-created (the production section-append path). const SAMPLE = `--- last_post_date: "2026-04-05" first_post_date: "2026-01-15" last_post_topic: "AI strategy" posts_this_week: 2 weekly_goal: 3 current_streak: 5 longest_streak: 12 current_week: "2026-W14" follower_count: 850 follower_target: 10000 target_date: "2026-12-31" --- # LinkedIn Session State ## Recent Posts - [2026-04-05] "AI governance is not about..." (1450) - AI strategy ## Milestone Log `; const WITH_FH = SAMPLE.replace( 'last_post_date: "2026-04-05"', () => 'last_post_date: "2026-04-05"\nlast_firsthour_date: null' ); const TEMPLATE = `--- last_post_date: "2026-04-05" last_post_topic: "AI strategy" posts_this_week: 2 follower_count: 850 follower_target: 10000 target_date: "2026-12-31" --- # LinkedIn Session State ## Recent Posts - [2026-04-05] "AI governance is not about..." (1450) - AI strategy ## First-Hour Plans ## Outreach Pipeline `; // Coverage battery: one or more cases per exported mutator. `fields` lists the // `.replace()` paths exercised; `expect` are substrings that MUST appear verbatim in // the output (each would differ if a `$`-token expanded). const BATTERY = [ { fn: "updatePostTracking", cases: [ { path: "scalar replaceField (:58) + Recent Posts append (:122)", run: () => mod.updatePostTracking(SAMPLE, { postDate: "2026-04-09", postTopic: PAYLOAD, hookText: PAYLOAD, charCount: 1200, format: "post", }), expect: [`last_post_topic: "${PAYLOAD}"`, `"${PAYLOAD}" (1200) - ${PAYLOAD}`], once: [/^## Recent Posts$/gm], }, ], }, { fn: "pruneContentHistory", cases: [ { path: "section rewrite of KEPT entries (:171)", run: () => { const today = new Date(); const old = new Date(today); old.setDate(old.getDate() - 100); const recent = new Date(today); recent.setDate(recent.getDate() - 10); const oldDate = old.toISOString().slice(0, 10); const recentDate = recent.toISOString().slice(0, 10); // Plant the payload via a FUNCTION replace so the fixture itself is not // pre-corrupted by the very expansion under test. const state = SAMPLE.replace( "## Recent Posts\n\n", () => `## Recent Posts\n\n- [${oldDate}] "drop" (1000) - drop me\n- [${recentDate}] "${PAYLOAD}" (1200) - ${PAYLOAD}\n` ); const r = mod.pruneContentHistory(state, 90); return { ...r, _recentDate: recentDate, _oldDate: oldDate }; }, assert: (r) => { assert.notEqual(r, null, "prune returned null"); assert.equal(r.pruned, 1, "exactly the old entry pruned"); assert.ok(!r.content.includes(r._oldDate), "old entry pruned"); assert.ok(r.content.includes(`- [${r._recentDate}] "${PAYLOAD}" (1200) - ${PAYLOAD}`), "kept $-entry survives verbatim"); }, once: [/^## Recent Posts$/gm], }, ], }, { fn: "updateFollowerCount", cases: [ { // No free-text field: count is an integer, month is a `YYYY-MM` date used in // date math. There is NO untrusted-string replacement surface here. Covered // for completeness with a structural-integrity assertion on benign input. path: "no untrusted-string replacement surface (count/month are int/date)", run: () => mod.updateFollowerCount(SAMPLE, { count: 920, month: "2026-04" }), expect: ["follower_count: 920", "[2026-04] 920 (+70)"], once: [/^## Milestone Log$/gm], }, ], }, { fn: "recordFirstHourPlan", cases: [ { path: "section append, free-text topic/targets/comments (:271)", run: () => mod.recordFirstHourPlan(TEMPLATE, { planDate: "2026-05-30 09:00", postTopic: PAYLOAD, targets: [PAYLOAD], draftComments: [PAYLOAD], plan: ["09:00 — live"], }), expect: [`### [2026-05-30 09:00] ${PAYLOAD}`, `- ${PAYLOAD}`], once: [/^## First-Hour Plans$/gm], }, { path: "date additive-insert (:246) — planDate fuzzed", run: () => mod.recordFirstHourPlan(SAMPLE, { planDate: PAYLOAD, postTopic: "t" }), expect: [`last_firsthour_date: "${PAYLOAD}"`], once: [/^last_firsthour_date:/gm], }, { path: "date scalar replaceField (:237) — planDate fuzzed", run: () => mod.recordFirstHourPlan(WITH_FH, { planDate: PAYLOAD, postTopic: "t" }), expect: [`last_firsthour_date: "${PAYLOAD}"`], once: [/^last_firsthour_date:/gm], }, ], }, { fn: "recordOutreachContact", cases: [ { path: "section append, free-text partner/stage/nextAction (:331)", run: () => mod.recordOutreachContact(TEMPLATE, { contactDate: "2026-05-30 14:00", track: "collab", partner: PAYLOAD, stage: PAYLOAD, nextAction: PAYLOAD, dueDate: "2026-06-06", }), expect: [`${PAYLOAD}`, `**Stage:** ${PAYLOAD}`, `**Next action:** ${PAYLOAD}`], once: [/^## Outreach Pipeline$/gm], }, { path: "date additive-insert via last_post_date anchor (:308) — contactDate fuzzed", run: () => mod.recordOutreachContact(SAMPLE, { contactDate: PAYLOAD, partner: "p" }), expect: [`last_outreach_date: "${PAYLOAD}"`], once: [/^last_outreach_date:/gm], }, ], }, ]; // --- Backstop 1: coverage completeness ------------------------------------------- // Every exported mutator must be in the battery, minus the documented I/O wrapper. // A new export added without `$`-coverage fails here. const IO_WRAPPER_EXEMPT = new Set(["writeState"]); const exportedFns = Object.keys(mod).filter((k) => typeof mod[k] === "function"); const covered = new Set(BATTERY.map((b) => b.fn)); const uncovered = exportedFns.filter((k) => !covered.has(k) && !IO_WRAPPER_EXEMPT.has(k)); let failed = 0; const failures = []; if (uncovered.length) { failed++; failures.push(`coverage gap: exported mutator(s) without $-safety coverage → ${uncovered.join(", ")} (add a battery case or document an exemption)`); } // Guard the exemption too: if writeState is renamed/removed, surface it rather than // silently exempting a stale name. for (const name of IO_WRAPPER_EXEMPT) { if (!exportedFns.includes(name)) { failed++; failures.push(`exemption stale: '${name}' is exempted but no longer exported — re-check the exemption list`); } } // --- Backstop 2: non-vacuity self-test ------------------------------------------- // Prove the payload is genuinely dangerous (string replace corrupts) and that the // fix shape (function replace) preserves it. A guard that cannot see the corruption // it forbids enforces nothing. { const subject = 'last_post_topic: "OLD"'; const naive = subject.replace(/^last_post_topic: .*/m, `last_post_topic: "${PAYLOAD}"`); const safe = subject.replace(/^last_post_topic: .*/m, () => `last_post_topic: "${PAYLOAD}"`); if (naive.includes(PAYLOAD)) { failed++; failures.push("self-test vacuous: a STRING replacement did NOT corrupt the payload — the payload no longer exercises `$`-expansion, so a PASS is meaningless"); } if (!safe.includes(PAYLOAD)) { failed++; failures.push("self-test broken: a FUNCTION replacement did NOT preserve the payload — the verbatim-survival assertion is unsound"); } } // --- Run the battery ------------------------------------------------------------- let cases = 0; for (const { fn, cases: list } of BATTERY) { for (const c of list) { cases++; try { const out = c.run(); if (c.assert) { c.assert(out); } else { const content = out && out.content; assert.ok(content, `${fn} [${c.path}]: no content returned`); for (const sub of c.expect || []) { assert.ok(content.includes(sub), `${fn} [${c.path}]: missing verbatim → ${JSON.stringify(sub)}`); } for (const re of c.once || []) { const hits = (content.match(re) || []).length; assert.equal(hits, 1, `${fn} [${c.path}]: structural anchor ${re} appeared ${hits}× (expected 1 — a $&/$1 expansion duplicated it)`); } } } catch (e) { failed++; failures.push(`${fn} [${c.path}]: ${e.message}`); } } } if (failed === 0) { console.log(`✓ $-safety: ${cases} adversarial case(s) across ${BATTERY.length} mutator(s) preserved the payload verbatim; coverage complete; self-test non-vacuous`); process.exit(0); } else { console.error(`✗ $-safety guard FAILED (${failed}):`); for (const f of failures) console.error(` - ${f}`); process.exit(1); }