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¶
- Create test user account
- Create test executor account
- Configure email delivery to test inboxes (Mailhog or similar)
- 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¶
- [ ]
armed→triggeredrequires 2+ signals - [ ]
triggered→pending_executionrespects challenge window - [ ]
pending_execution→executingrespects 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.