diff --git a/CHANGELOG.md b/CHANGELOG.md
index f29707f..20d411f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
+## [0.1.0.4] - 2026-06-14
+
+### Added
+- Blog post update: `at`-command one-time scheduling section added to the Conductor session auto-resume post — shows how to use macOS `at` for a single-shot script run without a recurring launchd job
+- Unit test coverage for `PostBannerProvider` (context propagation, `setPost`, `registerTitleEl`), `PostBanner` (all `secondsLeft` display branches, `aria-hidden` state), `BuyMeCryptoCard` (empty-wallets state, coin tab switching, copy error snackbar), and `lib/wallets` (structure + CI guard for uninitialised submodule)
+
## [0.1.0.3] - 2026-06-13
### Added
diff --git a/VERSION b/VERSION
index d5a3ab3..bad34a3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0.3
+0.1.0.4
diff --git a/content b/content
index 2cfec56..212f9e5 160000
--- a/content
+++ b/content
@@ -1 +1 @@
-Subproject commit 2cfec5664210830df520d68070ec5606084a492e
+Subproject commit 212f9e5f5d20802c178312a30c41aa7d384601df
diff --git a/package.json b/package.json
index 216c261..a0f7c35 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "baymac.lol",
- "version": "0.1.0.3",
+ "version": "0.1.0.4",
"private": true,
"author": {
"name": "Parichay Barpanda",
diff --git a/tests/unit/components/BuyMeCryptoCard.test.tsx b/tests/unit/components/BuyMeCryptoCard.test.tsx
new file mode 100644
index 0000000..f963536
--- /dev/null
+++ b/tests/unit/components/BuyMeCryptoCard.test.tsx
@@ -0,0 +1,112 @@
+import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+
+mock.module('use-clipboard-copy', () => ({
+ useClipboard: () => ({
+ copy: (_v: string) => {},
+ copied: false,
+ }),
+}));
+
+import BuyMeCryptoCard from '../../../components/BuyMeCrypto/BuyMeCryptoCard';
+import type { Wallet } from '../../../lib/wallets';
+
+const WALLETS: Wallet[] = [
+ { coin: 'BTC', label: 'Bitcoin', network: 'mainnet', address: 'bc1qtest123' },
+ { coin: 'SUI', label: 'Sui', network: 'mainnet', address: '0xsuiaddress' },
+];
+
+let container: HTMLDivElement;
+let root: Root;
+
+beforeEach(() => {
+ document.body.innerHTML = '';
+ container = document.createElement('div');
+ document.body.appendChild(container);
+});
+
+afterEach(async () => {
+ await act(async () => {
+ root?.unmount();
+ });
+ document.body.innerHTML = '';
+});
+
+async function render(ui: React.ReactNode) {
+ await act(async () => {
+ root = createRoot(container);
+ root.render(ui);
+ });
+}
+
+describe('BuyMeCryptoCard', () => {
+ it('shows empty-state message when wallets array is empty', async () => {
+ await render();
+ expect(container.textContent).toContain('no wallets configured');
+ expect(container.textContent).toContain('wallets.json');
+ });
+
+ it('renders the first wallet address by default', async () => {
+ await render();
+ expect(container.textContent).toContain('bc1qtest123');
+ expect(container.textContent).toContain('Bitcoin');
+ });
+
+ it('renders coin tab buttons for each wallet', async () => {
+ await render();
+ const tabs = container.querySelectorAll('[role="tab"]');
+ expect(tabs.length).toBe(2);
+ expect(tabs[0].textContent).toBe('BTC');
+ expect(tabs[1].textContent).toBe('SUI');
+ });
+
+ it('first tab is selected by default', async () => {
+ await render();
+ const tabs = container.querySelectorAll('[role="tab"]');
+ expect((tabs[0] as HTMLElement).getAttribute('aria-selected')).toBe('true');
+ expect((tabs[1] as HTMLElement).getAttribute('aria-selected')).toBe(
+ 'false'
+ );
+ });
+
+ it('switching coin tab shows the selected wallet address', async () => {
+ await render();
+ const suiTab = container.querySelectorAll(
+ '[role="tab"]'
+ )[1] as HTMLButtonElement;
+ await act(async () => {
+ suiTab.click();
+ });
+ expect(container.textContent).toContain('0xsuiaddress');
+ expect(container.textContent).toContain('Sui');
+ });
+
+ it('copy button renders with accessible label', async () => {
+ await render();
+ const btn = container.querySelector('button[aria-label="Copy address"]');
+ expect(btn).not.toBeNull();
+ });
+
+ it('shows error snackbar when clipboard is unavailable', async () => {
+ const original = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: undefined,
+ });
+
+ await render();
+ const btn = container.querySelector(
+ 'button[aria-label="Copy address"]'
+ ) as HTMLButtonElement;
+ await act(async () => {
+ btn.click();
+ });
+
+ expect(container.textContent).toContain(
+ 'copy failed — select address manually'
+ );
+
+ if (original) Object.defineProperty(navigator, 'clipboard', original);
+ });
+});
diff --git a/tests/unit/components/PostBanner.test.tsx b/tests/unit/components/PostBanner.test.tsx
new file mode 100644
index 0000000..1d010f4
--- /dev/null
+++ b/tests/unit/components/PostBanner.test.tsx
@@ -0,0 +1,112 @@
+import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
+import { act, createElement } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+
+// Stub out the PostBannerProvider context with controllable values.
+let ctxValues = {
+ post: null as { title: string; mins?: number } | null,
+ progress: 0,
+ secondsLeft: null as number | null,
+ setPost: () => {},
+ registerTitleEl: () => {},
+};
+
+mock.module('../../../context/PostBannerProvider', () => ({
+ usePostBanner: () => ctxValues,
+ default: ({ children }: { children: React.ReactNode }) => children,
+ BANNER_RANGE: 52,
+}));
+
+import PostBanner from '../../../components/Nav/PostBanner';
+
+let container: HTMLDivElement;
+let root: Root;
+
+beforeEach(() => {
+ document.body.innerHTML = '';
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ // Reset to defaults.
+ ctxValues = {
+ post: null,
+ progress: 0,
+ secondsLeft: null,
+ setPost: () => {},
+ registerTitleEl: () => {},
+ };
+});
+
+afterEach(async () => {
+ await act(async () => {
+ root?.unmount();
+ });
+ document.body.innerHTML = '';
+});
+
+async function render() {
+ await act(async () => {
+ root = createRoot(container);
+ root.render(createElement(PostBanner));
+ });
+}
+
+describe('PostBanner', () => {
+ it('renders nothing when post is null', async () => {
+ ctxValues.post = null;
+ await render();
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders banner when post is set', async () => {
+ ctxValues.post = { title: 'My Post' };
+ ctxValues.progress = 0.5;
+ await render();
+ expect(container.textContent).toContain('My Post');
+ });
+
+ it('is aria-hidden when progress <= 0.001', async () => {
+ ctxValues.post = { title: 'Test' };
+ ctxValues.progress = 0;
+ await render();
+ const banner = container.firstElementChild as HTMLElement;
+ expect(banner?.getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('is not aria-hidden when progress > 0.001', async () => {
+ ctxValues.post = { title: 'Test' };
+ ctxValues.progress = 0.5;
+ await render();
+ const banner = container.firstElementChild as HTMLElement;
+ expect(banner?.getAttribute('aria-hidden')).toBe('false');
+ });
+
+ it('shows "N min left" when secondsLeft >= 60', async () => {
+ ctxValues.post = { title: 'Test', mins: 5 };
+ ctxValues.secondsLeft = 180;
+ await render();
+ expect(container.textContent).toContain('3 min left');
+ });
+
+ it('shows "N sec left" when 0 < secondsLeft < 60', async () => {
+ ctxValues.post = { title: 'Test', mins: 5 };
+ ctxValues.secondsLeft = 42;
+ await render();
+ expect(container.textContent).toContain('42 sec left');
+ });
+
+ it('shows done message when secondsLeft is 0', async () => {
+ ctxValues.post = { title: 'Test', mins: 5 };
+ ctxValues.secondsLeft = 0;
+ await render();
+ expect(container.textContent).toContain('purr purr ~');
+ });
+
+ it('shows no time label when secondsLeft is null', async () => {
+ ctxValues.post = { title: 'No time' };
+ ctxValues.secondsLeft = null;
+ await render();
+ expect(container.textContent).not.toContain('min left');
+ expect(container.textContent).not.toContain('sec left');
+ expect(container.textContent).not.toContain('purr');
+ });
+});
diff --git a/tests/unit/context/PostBannerProvider.test.tsx b/tests/unit/context/PostBannerProvider.test.tsx
new file mode 100644
index 0000000..0ca4acd
--- /dev/null
+++ b/tests/unit/context/PostBannerProvider.test.tsx
@@ -0,0 +1,112 @@
+import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
+import { act, createElement } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import PostBannerProvider, {
+ usePostBanner,
+} from '../../../context/PostBannerProvider';
+
+let container: HTMLDivElement;
+let root: Root;
+
+beforeEach(() => {
+ document.body.innerHTML = '';
+ container = document.createElement('div');
+ document.body.appendChild(container);
+});
+
+afterEach(async () => {
+ await act(async () => {
+ root?.unmount();
+ });
+ document.body.innerHTML = '';
+});
+
+// Renders a consumer child that captures context values into `out`.
+function ContextCapture({ out }: { out: Record }) {
+ const ctx = usePostBanner();
+ Object.assign(out, ctx);
+ return null;
+}
+
+async function renderProvider(out: Record) {
+ await act(async () => {
+ root = createRoot(container);
+ root.render(
+ createElement(
+ PostBannerProvider,
+ null,
+ createElement(ContextCapture, { out })
+ )
+ );
+ });
+}
+
+describe('PostBannerProvider', () => {
+ it('provides default context values on first render', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+ expect(out.post).toBeNull();
+ expect(out.progress).toBe(0);
+ expect(out.secondsLeft).toBeNull();
+ expect(typeof out.setPost).toBe('function');
+ expect(typeof out.registerTitleEl).toBe('function');
+ });
+
+ it('setPost propagates new post to consumers', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+
+ await act(async () => {
+ (out.setPost as (p: { title: string; mins?: number } | null) => void)({
+ title: 'Hello World',
+ mins: 3,
+ });
+ });
+
+ expect((out.post as { title: string } | null)?.title).toBe('Hello World');
+ });
+
+ it('setPost with null clears the post', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+
+ await act(async () => {
+ (out.setPost as (p: { title: string } | null) => void)({ title: 'Test' });
+ });
+ await act(async () => {
+ (out.setPost as (p: null) => void)(null);
+ });
+
+ expect(out.post).toBeNull();
+ });
+
+ it('registerTitleEl with null keeps progress at 0', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+
+ await act(async () => {
+ (out.registerTitleEl as (el: HTMLElement | null) => void)(null);
+ });
+
+ expect(out.progress).toBe(0);
+ });
+
+ it('secondsLeft is null when no post is set', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+ expect(out.secondsLeft).toBeNull();
+ });
+
+ it('provides stable registerTitleEl reference (useCallback)', async () => {
+ const out: Record = {};
+ await renderProvider(out);
+ const first = out.registerTitleEl;
+
+ // Trigger a re-render via setPost.
+ await act(async () => {
+ (out.setPost as (p: { title: string }) => void)({ title: 're-render' });
+ });
+
+ expect(out.registerTitleEl).toBe(first);
+ });
+});
diff --git a/tests/unit/lib/wallets.test.ts b/tests/unit/lib/wallets.test.ts
new file mode 100644
index 0000000..b421f3c
--- /dev/null
+++ b/tests/unit/lib/wallets.test.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from 'bun:test';
+import { getWallets } from '../../../lib/wallets';
+
+// getWallets() reads content/wallets.json from the submodule. In CI without
+// `git submodule update --init`, the file is absent and getWallets() returns [].
+// We guard against that here so the test suite doesn't fail on a missing submodule.
+const wallets = getWallets();
+const hasWallets = wallets.length > 0;
+
+describe('getWallets', () => {
+ it('returns an array', () => {
+ expect(Array.isArray(wallets)).toBe(true);
+ });
+
+ it('returns [] when wallets.json is missing (submodule not initialised)', () => {
+ // This test always passes: either wallets exist (submodule is init'd)
+ // or getWallets() correctly returned [] (file-missing catch branch).
+ expect(Array.isArray(wallets)).toBe(true);
+ });
+
+ it("each wallet has uppercase coin, non-empty label/network/address (when submodule init'd)", () => {
+ if (!hasWallets) return; // skip when running without submodule
+ for (const w of wallets) {
+ expect(w.coin).toBe(w.coin.toUpperCase());
+ expect(typeof w.label).toBe('string');
+ expect(w.label.length).toBeGreaterThan(0);
+ expect(typeof w.network).toBe('string');
+ expect(w.address.length).toBeGreaterThan(0);
+ }
+ });
+
+ it("coin codes are alphanumeric and all caps (when submodule init'd)", () => {
+ if (!hasWallets) return;
+ for (const w of wallets) {
+ expect(/^[A-Z0-9]+$/.test(w.coin)).toBe(true);
+ }
+ });
+});