feat(v0.7): Walk-away Workbench (Band A + B) — multi-agent parallel build
Implements docs/PLAN_WALKAWAY_WORKBENCH.md (27 tasks, waves R0→W0→W1×14→W2→W3→W4)
via module-builder agents. 23 tasks built, 0 blocked.
Band A (finish the walk-away loop): A1 Web Push + lock-screen approve/deny
(web-push dep), A2 voice dictation, A3 quick-reply chips + saved-prompt palette,
A4 activity timeline, A5 stuck/idle alert.
Band B (workbench above the terminal): B1 read-only git diff viewer, B2 statusLine
telemetry → per-tab cost/context/PR gauges, B3 create git worktrees from the UI,
B4 plan-mode / permission-mode relay.
New: src/push/* (subscription store + VAPID push), src/http/{diff,statusline}.ts,
src/session/timeline.ts, public/{diff,timeline,quickreply,push-ui,...}.ts, sw-push,
statusLine script; extends hook intake, manager, server routes (Origin/CSRF guards
+ per-IP rate limits on state-changing ones; loopback-only ingest), terminal-session,
tabs, projects detail, service worker, setup-hooks (statusLine + ntfy bridge).
Orchestrator reconciled a W0 contract gap: added the 21 v0.7 Config fields to the
Config interface in types.ts (T-types had left them only in config.ts's return).
Verified: both tsc clean, full vitest + coverage 91.4/84.1/92.2/93.4 (≥80×4),
build:web OK. W4 review: no CRITICAL/HIGH; all security checks pass. Follow-ups
(non-blocking): move approve.mode validation into parseClientMessage, drop CSP
ws:/wss: wildcard, validate worktree base ref, +2 targeted tests.
This commit is contained in:
170
test/push/subscription-store.test.ts
Normal file
170
test/push/subscription-store.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import type { PushSubscriptionRecord } from '../../src/types.js'
|
||||
import {
|
||||
isValidSubscriptionRecord,
|
||||
loadSubscriptionStore,
|
||||
} from '../../src/push/subscription-store.js'
|
||||
|
||||
const dirs: string[] = []
|
||||
|
||||
function tmpFile(name = 'push-subs.json'): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'webterm-subs-'))
|
||||
dirs.push(dir)
|
||||
return join(dir, name)
|
||||
}
|
||||
|
||||
function rec(endpoint: string, createdAt = 1000): PushSubscriptionRecord {
|
||||
return { endpoint, keys: { p256dh: 'pub-' + endpoint, auth: 'auth-' + endpoint }, createdAt }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('isValidSubscriptionRecord', () => {
|
||||
it('accepts a well-formed record', () => {
|
||||
expect(isValidSubscriptionRecord(rec('https://push/1'))).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects non-objects and missing/invalid fields', () => {
|
||||
expect(isValidSubscriptionRecord(null)).toBe(false)
|
||||
expect(isValidSubscriptionRecord('x')).toBe(false)
|
||||
expect(isValidSubscriptionRecord({})).toBe(false)
|
||||
expect(isValidSubscriptionRecord({ endpoint: '', keys: { p256dh: 'a', auth: 'b' }, createdAt: 1 })).toBe(false)
|
||||
expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a' }, createdAt: 1 })).toBe(false)
|
||||
expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 1, auth: 'b' }, createdAt: 1 })).toBe(false)
|
||||
expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a', auth: 'b' } })).toBe(false)
|
||||
expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a', auth: 'b' }, createdAt: NaN })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadSubscriptionStore — loading', () => {
|
||||
it('starts empty when the file is missing', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
expect(store.list()).toEqual([])
|
||||
})
|
||||
|
||||
it('starts empty (and logs) on malformed JSON', () => {
|
||||
const err = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const path = tmpFile()
|
||||
writeFileSync(path, '{ not valid json')
|
||||
const store = loadSubscriptionStore(path, 50)
|
||||
expect(store.list()).toEqual([])
|
||||
expect(err).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('filters out invalid records on load', () => {
|
||||
const path = tmpFile()
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify([rec('https://push/ok'), { endpoint: 123 }, { foo: 'bar' }]),
|
||||
)
|
||||
const store = loadSubscriptionStore(path, 50)
|
||||
expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/ok'])
|
||||
})
|
||||
|
||||
it('returns empty when the JSON is not an array', () => {
|
||||
const path = tmpFile()
|
||||
writeFileSync(path, JSON.stringify({ endpoint: 'x' }))
|
||||
expect(loadSubscriptionStore(path, 50).list()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('SubscriptionStore — list immutability', () => {
|
||||
it('returns a frozen copy that cannot be mutated', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
store.add(rec('https://push/1'))
|
||||
const snapshot = store.list()
|
||||
expect(Object.isFrozen(snapshot)).toBe(true)
|
||||
expect(() => (snapshot as PushSubscriptionRecord[]).push(rec('https://push/2'))).toThrow()
|
||||
// original store is unaffected
|
||||
expect(store.list()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SubscriptionStore — add', () => {
|
||||
it('appends valid records', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
store.add(rec('https://push/1'))
|
||||
store.add(rec('https://push/2'))
|
||||
expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/1', 'https://push/2'])
|
||||
})
|
||||
|
||||
it('throws on an invalid record (fail-fast at the boundary)', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
expect(() => store.add({ endpoint: '' } as unknown as PushSubscriptionRecord)).toThrow()
|
||||
})
|
||||
|
||||
it('deduplicates by endpoint, replacing the prior record', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
store.add(rec('https://push/1', 1000))
|
||||
store.add({ endpoint: 'https://push/1', keys: { p256dh: 'new', auth: 'new' }, createdAt: 2000 })
|
||||
const list = store.list()
|
||||
expect(list).toHaveLength(1)
|
||||
expect(list[0]?.keys.p256dh).toBe('new')
|
||||
expect(list[0]?.createdAt).toBe(2000)
|
||||
})
|
||||
|
||||
it('enforces a FIFO cap at maxSubs, evicting the oldest', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 2)
|
||||
store.add(rec('https://push/1'))
|
||||
store.add(rec('https://push/2'))
|
||||
store.add(rec('https://push/3'))
|
||||
expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2', 'https://push/3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('SubscriptionStore — remove / prune', () => {
|
||||
it('removes a record by endpoint and is a no-op for unknown endpoints', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
store.add(rec('https://push/1'))
|
||||
store.add(rec('https://push/2'))
|
||||
store.remove('https://push/1')
|
||||
store.remove('https://push/absent')
|
||||
expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2'])
|
||||
})
|
||||
|
||||
it('prunes a batch of dead endpoints', () => {
|
||||
const store = loadSubscriptionStore(tmpFile(), 50)
|
||||
store.add(rec('https://push/1'))
|
||||
store.add(rec('https://push/2'))
|
||||
store.add(rec('https://push/3'))
|
||||
store.prune(['https://push/1', 'https://push/3'])
|
||||
expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('SubscriptionStore — persist', () => {
|
||||
it('writes the file with 0600 permissions and round-trips through load', async () => {
|
||||
const path = tmpFile()
|
||||
const store = loadSubscriptionStore(path, 50)
|
||||
store.add(rec('https://push/1', 11))
|
||||
store.add(rec('https://push/2', 22))
|
||||
await store.persist()
|
||||
|
||||
const mode = statSync(path).mode & 0o777
|
||||
expect(mode).toBe(0o600)
|
||||
|
||||
const onDisk = JSON.parse(readFileSync(path, 'utf8'))
|
||||
expect(onDisk.map((r: PushSubscriptionRecord) => r.endpoint)).toEqual([
|
||||
'https://push/1',
|
||||
'https://push/2',
|
||||
])
|
||||
|
||||
const reloaded = loadSubscriptionStore(path, 50)
|
||||
expect(reloaded.list().map((r) => r.endpoint)).toEqual(['https://push/1', 'https://push/2'])
|
||||
})
|
||||
|
||||
it('logs but does not throw when the destination is unwritable', async () => {
|
||||
const err = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
// a path whose parent directory does not exist
|
||||
const store = loadSubscriptionStore(join(tmpFile(), 'nope', 'subs.json'), 50)
|
||||
store.add(rec('https://push/1'))
|
||||
await expect(store.persist()).resolves.toBeUndefined()
|
||||
expect(err).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user