Skip to content

North Star Acceptance Test — Phase 0.5 Validation

Purpose: Single end-to-end test that validates the core flow works correctly. Credit: Test design from GPT5.2 review.


The Test

Run this end-to-end in staging with test email addresses.

Setup

  1. Create test user account
  2. Create test executor account
  3. Configure email delivery to test inboxes (Mailhog or similar)
  4. Set short intervals for testing (minutes instead of days)

Test Flow

┌─────────────────────────────────────────────────────────────────────┐
│  NORTH STAR ACCEPTANCE TEST                                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Step 1: Setup                                                      │
│  ────────────────                                                   │
│  • User creates vault                                               │
│  • User uploads encrypted document                                  │
│  • User arms dead man's switch (short interval for testing)        │
│  • User assigns executor                                           │
│                                                                     │
│  VERIFY: Trigger status = 'armed'                                  │
│  VERIFY: Audit log shows: vault_created, document_uploaded,        │
│          trigger_armed, executor_assigned                          │
│                                                                     │
│  Step 2: Check-in Missed                                           │
│  ────────────────────────                                          │
│  • User does NOT check in                                          │
│  • Check-in interval expires                                       │
│                                                                     │
│  VERIFY: Reminder email sent to user                               │
│  VERIFY: Trigger status still = 'armed' (not triggered yet)       │
│  VERIFY: Audit log shows: reminder_sent                            │
│                                                                     │
│  Step 3: Reminder Ignored                                          │
│  ─────────────────────────                                         │
│  • User does NOT click reminder link                               │
│  • Grace period begins                                             │
│                                                                     │
│  VERIFY: Second signal confirmed (reminder_unacknowledged)        │
│  VERIFY: Trigger transitions to 'triggered'                       │
│  VERIFY: Challenge window notification sent                       │
│  VERIFY: Audit log shows: trigger_fired, challenge_window_started │
│                                                                     │
│  Step 4: Challenge Window Passes                                   │
│  ────────────────────────────────                                  │
│  • User continues to do nothing                                    │
│  • Challenge window expires                                        │
│                                                                     │
│  VERIFY: Trigger transitions to 'pending_execution'               │
│  VERIFY: Final warning sent                                       │
│  VERIFY: Audit log shows: challenge_window_elapsed                │
│                                                                     │
│  Step 5: Execution                                                 │
│  ─────────────────                                                 │
│  • Abort window expires                                            │
│  • Trigger executes                                                │
│                                                                     │
│  VERIFY: Trigger status = 'executing' then 'released'             │
│  VERIFY: Executor receives access email with download link        │
│  VERIFY: Download link works and retrieves ciphertext             │
│  VERIFY: Audit log shows: execution_started, access_granted,      │
│          executor_notified, execution_completed                    │
│                                                                     │
│  Step 6: Abort Path (Separate Test Run)                           │
│  ────────────────────────────────────────                          │
│  • Reset: Create new vault, arm trigger                           │
│  • Miss check-in, ignore reminder                                 │
│  • Trigger enters 'triggered' state                               │
│  • User logs in and clicks ABORT                                  │
│                                                                     │
│  VERIFY: Trigger transitions to 'aborted'                         │
│  VERIFY: Executor does NOT receive access                         │
│  VERIFY: Audit log shows: abort_requested, trigger_aborted        │
│  VERIFY: Audit trail shows exactly what happened and when         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Assertions

Timing Assertions

  • [ ] Reminder sent within 1 minute of check-in expiry
  • [ ] State transitions happen within expected windows
  • [ ] No premature transitions (respects configured intervals)

State Machine Assertions

  • [ ] armedtriggered requires 2+ signals
  • [ ] triggeredpending_execution respects challenge window
  • [ ] pending_executionexecuting respects abort window
  • [ ] Abort works from armed, triggered, pending_execution
  • [ ] Abort does NOT work from executing (unless partial)

Audit Trail Assertions

  • [ ] Every state transition has audit entry
  • [ ] Audit entries have correct timestamps
  • [ ] Audit entries have correct user/actor attribution
  • [ ] No gaps in sequence numbers
  • [ ] Hash chain is valid

Notification Assertions

  • [ ] Reminder email delivered
  • [ ] Challenge notification delivered
  • [ ] Final warning delivered
  • [ ] Executor access email delivered
  • [ ] All emails have correct content

Access Assertions

  • [ ] Executor cannot access vault before trigger executes
  • [ ] Executor CAN access vault after trigger executes
  • [ ] Download link is time-limited
  • [ ] Downloaded content matches uploaded content (ciphertext)

Test Configuration

For testing, use short intervals:

const TEST_CONFIG = {
  checkInInterval: 2 * 60 * 1000,      // 2 minutes (not 7 days)
  gracePeriod: 1 * 60 * 1000,          // 1 minute (not 3 days)
  challengeWindow: 1 * 60 * 1000,      // 1 minute (not 7 days)
  abortWindow: 30 * 1000,              // 30 seconds (not 24 hours)
  downloadLinkExpiry: 5 * 60 * 1000,   // 5 minutes
};

Total test duration: ~5 minutes for full flow.


Automated Test Script

describe('North Star Acceptance Test', () => {
  let user: TestUser;
  let executor: TestUser;
  let vaultId: string;
  let triggerId: string;

  beforeAll(async () => {
    user = await createTestUser();
    executor = await createTestUser();
  });

  test('Step 1: Setup', async () => {
    // Create vault
    const vault = await user.createVault();
    vaultId = vault.id;

    // Upload document
    await user.uploadDocument(vaultId, testDocument);

    // Arm trigger
    const trigger = await user.armTrigger(vaultId, {
      type: 'dead_man_switch',
      config: TEST_CONFIG,
      executor: executor.id,
    });
    triggerId = trigger.id;

    // Verify
    expect(trigger.status).toBe('armed');
    const audit = await getAuditLog(vaultId);
    expect(audit).toContainActions([
      'vault_created',
      'document_uploaded',
      'trigger_armed',
      'executor_assigned'
    ]);
  });

  test('Step 2: Check-in missed', async () => {
    // Wait for check-in to expire
    await wait(TEST_CONFIG.checkInInterval + 1000);

    // Verify reminder sent
    const emails = await getEmails(user.email);
    expect(emails).toContainEmail({ type: 'check_in_reminder' });

    // Trigger should still be armed (single signal)
    const trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('armed');
  });

  test('Step 3: Reminder ignored, trigger fires', async () => {
    // Wait for grace period
    await wait(TEST_CONFIG.gracePeriod + 1000);

    // Verify trigger transitioned
    const trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('triggered');

    // Verify audit
    const audit = await getAuditLog(triggerId);
    expect(audit).toContainActions([
      'reminder_sent',
      'reminder_unacknowledged',
      'trigger_fired',
      'challenge_window_started'
    ]);
  });

  test('Step 4: Challenge window passes', async () => {
    await wait(TEST_CONFIG.challengeWindow + 1000);

    const trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('pending_execution');
  });

  test('Step 5: Execution', async () => {
    await wait(TEST_CONFIG.abortWindow + 1000);

    // Verify execution completed
    const trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('released');

    // Verify executor received access
    const emails = await getEmails(executor.email);
    const accessEmail = emails.find(e => e.type === 'executor_access');
    expect(accessEmail).toBeDefined();

    // Verify download works
    const downloadUrl = extractDownloadUrl(accessEmail);
    const downloaded = await fetch(downloadUrl);
    expect(downloaded.ok).toBe(true);

    // Verify audit trail complete
    const audit = await getAuditLog(triggerId);
    expect(audit).toContainActions([
      'execution_started',
      'access_granted',
      'executor_notified',
      'execution_completed'
    ]);
  });
});

describe('Abort Path', () => {
  test('User can abort during triggered state', async () => {
    // Setup new vault and trigger
    const { triggerId } = await setupArmedTrigger();

    // Let trigger fire
    await wait(TEST_CONFIG.checkInInterval + TEST_CONFIG.gracePeriod + 2000);

    // Verify triggered
    let trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('triggered');

    // User aborts
    await user.abortTrigger(triggerId);

    // Verify aborted
    trigger = await getTrigger(triggerId);
    expect(trigger.status).toBe('aborted');

    // Verify executor did NOT get access
    const emails = await getEmails(executor.email);
    expect(emails).not.toContainEmail({ type: 'executor_access' });

    // Verify audit shows abort
    const audit = await getAuditLog(triggerId);
    expect(audit).toContainActions(['abort_requested', 'trigger_aborted']);
  });
});

describe('Audit Trail Integrity', () => {
  test('Hash chain is valid and complete', async () => {
    // After any test run, verify audit integrity
    const audit = await getFullAuditLog(vaultId);

    // 1. Verify no sequence gaps
    for (let i = 1; i < audit.length; i++) {
      expect(audit[i].sequence).toBe(audit[i - 1].sequence + 1);
    }

    // 2. Verify hash chain
    for (let i = 1; i < audit.length; i++) {
      const expectedPreviousHash = audit[i - 1].hash;
      expect(audit[i].previousHash).toBe(expectedPreviousHash);

      // Verify hash computation
      const computedHash = await computeAuditHash(audit[i]);
      expect(audit[i].hash).toBe(computedHash);
    }

    // 3. Verify signatures
    for (const entry of audit) {
      const isValid = await verifySignature(entry, servicePublicKey);
      expect(isValid).toBe(true);
    }
  });

  test('No audit entries can be deleted or modified', async () => {
    const auditBefore = await getFullAuditLog(vaultId);
    const countBefore = auditBefore.length;

    // Attempt to delete (should fail due to RLS/permissions)
    await expect(
      db.delete('audit_log', { id: auditBefore[0].id })
    ).rejects.toThrow();

    // Attempt to update (should fail due to RLS/permissions)
    await expect(
      db.update('audit_log', { id: auditBefore[0].id }, { type: 'tampered' })
    ).rejects.toThrow();

    // Verify count unchanged
    const auditAfter = await getFullAuditLog(vaultId);
    expect(auditAfter.length).toBe(countBefore);
  });
});

// Helper: Compute audit entry hash (same algorithm as production)
async function computeAuditHash(entry: AuditEntry): Promise<string> {
  const hashInput = JSON.stringify({
    id: entry.id,
    sequence: entry.sequence,
    timestamp: entry.timestamp,
    previousHash: entry.previousHash,
    type: entry.type,
    resourceType: entry.resourceType,
    resourceId: entry.resourceId,
    actor: entry.actor,
    payload: entry.payload,
  });
  const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashInput));
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// Helper: Verify service signature
async function verifySignature(entry: AuditEntry, publicKey: CryptoKey): Promise<boolean> {
  const signatureData = entry.hash;
  const signature = Buffer.from(entry.signature, 'base64');
  return await crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    publicKey,
    signature,
    new TextEncoder().encode(signatureData)
  );
}

Pass Criteria

The test passes if: 1. All steps complete without error 2. All assertions pass 3. State machine follows expected transitions 4. Audit trail is complete and valid 5. Abort path works correctly


What This Test Validates

  • Core flow works: Upload → Arm → Miss → Trigger → Execute → Access
  • Multi-signal required: Single missed check-in doesn't trigger
  • Abort works: User can stop execution at any abortable stage
  • Audit is complete: Every action is logged
  • Notifications deliver: All parties receive correct emails

What This Test Does NOT Validate

  • UI/UX (manual testing required)
  • Encryption correctness (separate crypto tests)
  • Load/performance (separate load tests)
  • Multi-user scenarios (separate integration tests)
  • Edge cases (separate edge case tests)

This is the minimum viable proof that the system works.


If this test passes, we have a working dead man's switch. Everything else is enhancement.