A run entry on GET /public/events now names its run, the same id the event page already publishes on each occurrence and `?run=` takes. A Rust map marker carries core's run id and nothing else about its event, so without this the app could only find the event by fetching every event page. A projected entry has no runId: nothing is committed to it. Rehearsals and unlisted events stay absent from the calendar, so their markers stay unlinked. The web calendar ignores the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
522 lines
21 KiB
JavaScript
522 lines
21 KiB
JavaScript
// ── The public event surface (EVENTS_PLAN.md Phase 14a) ────────────────────
|
|
//
|
|
// The phase's shipped claim: **a visitor with no account sees the calendar, one
|
|
// event's page and an arc, and sees nothing an operator did not announce.**
|
|
//
|
|
// What is worth testing here is almost entirely the second half. The reads
|
|
// themselves are joins; the decisions are about what is absent:
|
|
//
|
|
// • a rehearsal, an unlisted definition and a draft are absent from every
|
|
// surface, and absent the same way — a 404 that cannot be told from a slug
|
|
// that never existed
|
|
// • the plan behind an event (phases, steps, actions, params) is never
|
|
// published; a live run carries the LABEL of its phase and nothing else
|
|
// • `failed` and `missed` are published as `cancelled`, because the difference
|
|
// between them is about the deployment rather than about the event
|
|
// • `member_key` never leaves the server, even on a results table
|
|
// • a participant's own history obeys the same two exclusions as the calendar,
|
|
// so attending an unannounced event does not disclose that it exists
|
|
//
|
|
// Stubbed at the `.db` layer, the shape `eventSchedule.test.js` uses.
|
|
|
|
process.env.DB_HOST = '127.0.0.1'
|
|
process.env.DB_PORT = '59999'
|
|
|
|
const { test, beforeEach, afterEach, after } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const publicModel = require('../src/model/events/eventPublic.model')
|
|
const definitionsDb = require('../src/model/events/eventDefinitions.db')
|
|
const runsDb = require('../src/model/events/eventRuns.db')
|
|
const seriesDb = require('../src/model/events/eventSeries.db')
|
|
const versionsDb = require('../src/model/events/eventVersions.db')
|
|
const participantsDb = require('../src/model/events/eventRunParticipants.db')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
const NOW = new Date('2026-09-01T12:00:00Z')
|
|
|
|
const SPEC = {
|
|
schedule: { kind: 'manual' },
|
|
phases: [
|
|
{ id: 'muster', label: 'The muster', steps: [{ id: 's1', action: 'core.announce' }] },
|
|
{ id: 'assault', label: 'The assault', steps: [] },
|
|
],
|
|
}
|
|
|
|
const originals = {}
|
|
for (const [name, mod] of [
|
|
['definitionsDb', definitionsDb],
|
|
['runsDb', runsDb],
|
|
['seriesDb', seriesDb],
|
|
['versionsDb', versionsDb],
|
|
['participantsDb', participantsDb],
|
|
]) {
|
|
originals[name] = { mod, fns: { ...mod } }
|
|
}
|
|
const restoreOriginals = () => {
|
|
for (const { mod, fns } of Object.values(originals)) Object.assign(mod, fns)
|
|
}
|
|
|
|
let store
|
|
|
|
const definition = (over = {}) => ({
|
|
id: 1,
|
|
title: 'The Yew Invasion',
|
|
slug: 'the-yew-invasion',
|
|
summary: 'Orcish warbands are massing north of Yew.',
|
|
body: '<p>They came at dusk.</p>',
|
|
image_url: null,
|
|
state: 'ready',
|
|
listed: true,
|
|
timezone: 'America/New_York',
|
|
series_id: null,
|
|
series_name: null,
|
|
series_slug: null,
|
|
current_version_id: 100,
|
|
spec: SPEC,
|
|
...over,
|
|
})
|
|
|
|
const run = (over = {}) => ({
|
|
id: 3692,
|
|
definition_id: 1,
|
|
version_id: 100,
|
|
scope: '',
|
|
status: 'completed',
|
|
// Every one of these is a field the public shapes must NOT carry. They are on
|
|
// the fixture on purpose: a `{ ...run }` anywhere in the model would publish
|
|
// them, and the assertions below are what would catch it.
|
|
health: 'degraded',
|
|
cleanup_status: 'incomplete',
|
|
claimed_by: 'worker-3',
|
|
claim_expires_at: new Date(),
|
|
last_error: 'sidecar responded 503',
|
|
current_phase: 'assault',
|
|
scheduled_for: new Date('2026-08-29T00:00:00Z'),
|
|
started_at: new Date('2026-08-29T00:00:05Z'),
|
|
ended_at: new Date('2026-08-29T01:30:00Z'),
|
|
timezone: 'America/New_York',
|
|
rehearsal: false,
|
|
results_published_at: new Date('2026-08-29T02:00:00Z'),
|
|
...over,
|
|
})
|
|
|
|
function installStubs() {
|
|
definitionsDb.getPublicBySlug = async (slug) => {
|
|
const d = store.definitions.find((x) => x.slug === slug)
|
|
return d && d.state === 'ready' && d.listed ? d : undefined
|
|
}
|
|
definitionsDb.listPublicBySeries = async (seriesId) =>
|
|
store.definitions.filter((d) => d.series_id === seriesId && d.state === 'ready' && d.listed)
|
|
definitionsDb.findSchedulable = async ({ listedOnly = false } = {}) =>
|
|
store.definitions
|
|
.filter((d) => d.state === 'ready' && (!listedOnly || d.listed))
|
|
.map((d) => ({ ...d, version_spec: d.spec }))
|
|
|
|
// Mirrors the OVERLAP predicate the real statement uses: a run is in the window
|
|
// if its instant falls inside it, OR if it began before the window and is still
|
|
// live. A run is an interval, not an instant — see `eventRuns.db.listInWindow`.
|
|
runsDb.listInWindow = async ({ from, to, publicOnly = false }) =>
|
|
store.runs.filter((r) => {
|
|
const at = new Date(r.scheduled_for)
|
|
const startsInside = at >= from && at < to
|
|
const liveAcross = at < to && ['starting', 'running', 'paused', 'ending'].includes(r.status)
|
|
if (!startsInside && !liveAcross) return false
|
|
if (!publicOnly) return true
|
|
const d = store.definitions.find((x) => x.id === r.definition_id)
|
|
return !r.rehearsal && d && d.listed && d.state !== 'archived'
|
|
})
|
|
runsDb.listPublicForDefinition = async (id) =>
|
|
store.runs
|
|
.filter((r) => r.definition_id === id && !r.rehearsal)
|
|
.sort((a, b) => new Date(b.scheduled_for) - new Date(a.scheduled_for))
|
|
|
|
seriesDb.getById = async (id) => store.series.find((s) => s.id === id) || null
|
|
seriesDb.getBySlug = async (slug) => store.series.find((s) => s.slug === slug) || null
|
|
versionsDb.getById = async (id) => (id === 100 ? { id, spec: SPEC } : null)
|
|
participantsDb.listForRun = async () => store.participants
|
|
participantsDb.listForUser = async () => store.history
|
|
}
|
|
|
|
beforeEach(() => {
|
|
const d = definition()
|
|
store = {
|
|
definitions: [d],
|
|
// The joined shape `listInWindow` answers with.
|
|
runs: [{ ...run(), definition_title: d.title, definition_slug: d.slug }],
|
|
series: [],
|
|
participants: [],
|
|
history: [],
|
|
}
|
|
installStubs()
|
|
})
|
|
afterEach(restoreOriginals)
|
|
|
|
// ── The calendar ───────────────────────────────────────────────────────────
|
|
|
|
test('a calendar entry carries no operational field at all', async () => {
|
|
const result = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW })
|
|
assert.equal(result.ok, true)
|
|
const [entry] = result.entries
|
|
assert.equal(entry.title, 'The Yew Invasion')
|
|
// The whole security property of this file, asserted positively: the entry has
|
|
// exactly these keys and gaining one is a deliberate act.
|
|
assert.deepEqual(Object.keys(entry).sort(), [
|
|
'kind', 'live', 'runId', 'scheduledFor', 'seriesName', 'seriesSlug', 'slug', 'status', 'timezone',
|
|
'title',
|
|
])
|
|
})
|
|
|
|
test('a run entry names its run, and a projection names none', async () => {
|
|
// Rust phase 15, D125: a map marker carries core's run id, and the app finds
|
|
// the event it belongs to from this calendar. The id is the one the event page
|
|
// already publishes on each occurrence.
|
|
store.definitions[0].spec = {
|
|
...SPEC,
|
|
schedule: { kind: 'weekly', days: ['saturday'], time: '00:00' },
|
|
}
|
|
const result = await publicModel.calendar({ from: '2026-08-28', to: '2026-09-15', now: NOW })
|
|
const runs = result.entries.filter((e) => e.kind === 'run')
|
|
const projected = result.entries.filter((e) => e.kind === 'projected')
|
|
assert.equal(runs.length, 1)
|
|
assert.equal(runs[0].runId, store.runs[0].id)
|
|
assert.ok(projected.length > 0, 'the weekly schedule must forecast past the one run')
|
|
for (const entry of projected) assert.equal('runId' in entry, false)
|
|
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
const occurrences = [page.event.current, page.event.next, ...page.event.upcoming, ...page.event.past]
|
|
assert.ok(occurrences.some((o) => o && o.runId === runs[0].runId))
|
|
})
|
|
|
|
test('the default window reaches back as well as forward', async () => {
|
|
// §I: this route is "upcoming, live and recent". The default used to start at
|
|
// `now`, which left no room for the third word — an event that finished an hour
|
|
// ago was already gone, so a visitor had nowhere to find the results of the
|
|
// thing they had just attended (Phase 16 walk).
|
|
const result = await publicModel.calendar({ now: NOW })
|
|
assert.equal(result.ok, true)
|
|
const back = (NOW - new Date(result.window.from)) / 86_400_000
|
|
const forward = (new Date(result.window.to) - NOW) / 86_400_000
|
|
assert.equal(back, publicModel.DEFAULT_RECENT_DAYS)
|
|
assert.equal(forward, publicModel.DEFAULT_WINDOW_DAYS)
|
|
})
|
|
|
|
test('a run happening RIGHT NOW is on the calendar, whenever it started', async () => {
|
|
// The defect this pair was written for: the site said `live: true` on the
|
|
// event's own page and served `entries: []` from the calendar, because the
|
|
// window test read the START instant and a live run had already started. A run
|
|
// is an interval; the calendar asks which intervals overlap it.
|
|
store.runs = [
|
|
{
|
|
...run({
|
|
status: 'running',
|
|
// Well before any default window would begin.
|
|
scheduled_for: new Date('2026-08-01T00:00:00Z'),
|
|
ended_at: null,
|
|
}),
|
|
definition_title: 'The Yew Invasion',
|
|
definition_slug: 'the-yew-invasion',
|
|
},
|
|
]
|
|
const result = await publicModel.calendar({ now: NOW })
|
|
assert.equal(result.ok, true)
|
|
const entry = result.entries.find((e) => e.kind === 'run')
|
|
assert.ok(entry, 'a live run must appear however long ago it began')
|
|
assert.equal(entry.live, true)
|
|
assert.equal(entry.status, 'live')
|
|
})
|
|
|
|
test('a run that finished inside the recent tail is still on the calendar', async () => {
|
|
store.runs = [
|
|
{
|
|
...run({
|
|
status: 'completed',
|
|
scheduled_for: new Date(NOW.getTime() - 2 * 86_400_000),
|
|
ended_at: new Date(NOW.getTime() - 2 * 86_400_000 + 3_600_000),
|
|
}),
|
|
definition_title: 'The Yew Invasion',
|
|
definition_slug: 'the-yew-invasion',
|
|
},
|
|
]
|
|
const result = await publicModel.calendar({ now: NOW })
|
|
assert.equal(result.ok, true)
|
|
assert.equal(result.entries.filter((e) => e.kind === 'run').length, 1)
|
|
})
|
|
|
|
test('nothing is FORECAST into the recent tail', async () => {
|
|
// The tail is for what happened, and only the materialised half fills it. A
|
|
// projection into the past would advertise an occurrence that did not happen:
|
|
// one that WAS created is a real row and arrives as a run, and one that was not
|
|
// is a slot the runner has already gone past.
|
|
store.definitions = [
|
|
definition({
|
|
spec: {
|
|
...SPEC,
|
|
schedule: {
|
|
kind: 'weekly',
|
|
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
|
time: '20:00',
|
|
},
|
|
},
|
|
}),
|
|
]
|
|
store.runs = []
|
|
const result = await publicModel.calendar({ now: NOW })
|
|
assert.equal(result.ok, true)
|
|
const projected = result.entries.filter((e) => e.kind !== 'run')
|
|
assert.ok(projected.length > 0, 'a daily schedule must still forecast forwards')
|
|
for (const entry of projected) {
|
|
assert.ok(
|
|
new Date(entry.scheduledFor) >= NOW,
|
|
`forecast ${entry.scheduledFor} is before now — the tail must hold no projections`,
|
|
)
|
|
}
|
|
})
|
|
|
|
test('a window wider than the cap is refused rather than served slowly', async () => {
|
|
const result = await publicModel.calendar({ from: '2026-01-01', to: '2026-12-31', now: NOW })
|
|
assert.equal(result.ok, false)
|
|
assert.equal(result.status, 400)
|
|
})
|
|
|
|
test('a projection is not emitted for an instant a run already occupies', async () => {
|
|
// The definition recurs weekly on the Saturday its one run already sits on.
|
|
store.definitions[0].spec = {
|
|
...SPEC,
|
|
schedule: { kind: 'weekly', days: ['saturday'], time: '00:00' },
|
|
}
|
|
const result = await publicModel.calendar({ from: '2026-08-28', to: '2026-08-31', now: NOW })
|
|
const at = result.entries.filter(
|
|
(e) => new Date(e.scheduledFor).getTime() === new Date('2026-08-29T00:00:00Z').getTime(),
|
|
)
|
|
assert.equal(at.length, 1)
|
|
assert.equal(at[0].kind, 'run')
|
|
})
|
|
|
|
// ── What the public never sees ─────────────────────────────────────────────
|
|
|
|
test('an unlisted event is absent from the calendar and 404s on its own page', async () => {
|
|
store.definitions[0].listed = false
|
|
const cal = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW })
|
|
assert.deepEqual(cal.entries, [])
|
|
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.ok, false)
|
|
assert.equal(page.status, 404)
|
|
})
|
|
|
|
test('a draft answers exactly as an unlisted one does — indistinguishable from no such slug', async () => {
|
|
store.definitions[0].state = 'draft'
|
|
const draft = await publicModel.event('the-yew-invasion')
|
|
const missing = await publicModel.event('no-such-event')
|
|
assert.deepEqual(draft, missing)
|
|
})
|
|
|
|
test('a rehearsal is absent from the calendar and from an event page', async () => {
|
|
store.runs[0].rehearsal = true
|
|
const cal = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW })
|
|
assert.deepEqual(cal.entries, [])
|
|
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.ok, true)
|
|
assert.deepEqual(page.event.past, [])
|
|
})
|
|
|
|
test('an occurrence publishes no health, no cleanup state, no claim and no error', async () => {
|
|
store.runs[0].status = 'running'
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
const occurrence = page.event.current
|
|
for (const leaked of ['health', 'cleanupStatus', 'claimedBy', 'claimExpiresAt', 'lastError', 'versionId']) {
|
|
assert.equal(occurrence[leaked], undefined, `${leaked} must not be published`)
|
|
}
|
|
assert.equal(JSON.stringify(page).includes('sidecar responded 503'), false)
|
|
})
|
|
|
|
test('a live run carries its phase LABEL, and never the spec behind it', async () => {
|
|
store.runs[0].status = 'running'
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.current.phase, 'The assault')
|
|
// The step ids in the fixture spec are the tell: if the spec were published
|
|
// anywhere in this answer, this would find it.
|
|
assert.equal(JSON.stringify(page).includes('core.announce'), false)
|
|
})
|
|
|
|
test('a phase the pinned version does not name renders nothing rather than an id', async () => {
|
|
store.runs[0].status = 'running'
|
|
store.runs[0].current_phase = 'a-phase-since-renamed'
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.current.phase, null)
|
|
})
|
|
|
|
test('failed and missed are both published as cancelled', async () => {
|
|
for (const status of ['failed', 'missed']) {
|
|
store.runs[0].status = status
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.past[0].status, 'cancelled', status)
|
|
}
|
|
})
|
|
|
|
test('paused is published as live — an operator holding a run is not a public state', async () => {
|
|
store.runs[0].status = 'paused'
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.current.status, 'live')
|
|
assert.equal(page.event.live, true)
|
|
})
|
|
|
|
// ── Which side of now an occurrence falls on ───────────────────────────────
|
|
//
|
|
// Both of these were found by the browser walk, and both are the same mistake:
|
|
// the split reading a STATUS where it should read a clock. Dates here are
|
|
// relative to the real clock, because `event()` asks `Date.now()` — a run
|
|
// "next Friday" has to still be next Friday when this runs.
|
|
|
|
const inDays = (n) => new Date(Date.now() + n * 86_400_000)
|
|
|
|
test('a cancelled occurrence in the FUTURE is what is coming, not what happened', async () => {
|
|
// It was announced and it has been called off, and "next Friday is off" is
|
|
// exactly what somebody checking the calendar came to find out. Filing it
|
|
// under "previously" tells them it already happened, which is the one thing
|
|
// certainly untrue about it.
|
|
store.runs = [
|
|
{ ...run({ id: 1, status: 'cancelled', scheduled_for: inDays(4), ended_at: null, results_published_at: null }) },
|
|
{ ...run({ id: 2, status: 'scheduled', scheduled_for: inDays(11), ended_at: null, results_published_at: null }) },
|
|
]
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.deepEqual(page.event.upcoming.map((o) => o.runId), [1, 2])
|
|
assert.deepEqual(page.event.past, [])
|
|
})
|
|
|
|
test('`next` skips a cancelled occurrence even though it is listed as coming', async () => {
|
|
// The headline answers "when is the next one", and a cancelled occurrence is
|
|
// not one. An event whose only future occurrence was called off has no `next`
|
|
// and says so, while the cancellation is still listed below.
|
|
store.runs = [
|
|
{ ...run({ id: 1, status: 'cancelled', scheduled_for: inDays(4), ended_at: null, results_published_at: null }) },
|
|
]
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.next, null)
|
|
assert.equal(page.event.upcoming.length, 1)
|
|
})
|
|
|
|
test('a scheduled occurrence whose moment has gone by is in the past', async () => {
|
|
// The other direction of the same rule: the runner had not reached it, so its
|
|
// status still says `scheduled` while the evening it named is over.
|
|
store.runs = [
|
|
{ ...run({ id: 1, status: 'scheduled', scheduled_for: inDays(-3), ended_at: null, results_published_at: null }) },
|
|
]
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.deepEqual(page.event.upcoming, [])
|
|
assert.deepEqual(page.event.past.map((o) => o.runId), [1])
|
|
})
|
|
|
|
// ── Results ────────────────────────────────────────────────────────────────
|
|
|
|
test('a results row publishes the score and the rank and never the member key', async () => {
|
|
store.participants = [
|
|
{ id: 1, member_key: 'serial:0x40001234', user_id: 7, score: 1420, rank_at: 1, meta: { name: 'Aldric' } },
|
|
]
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.results.participants[0].name, 'Aldric')
|
|
assert.equal(page.event.results.participants[0].rank, 1)
|
|
assert.equal(JSON.stringify(page).includes('0x40001234'), false)
|
|
assert.equal(JSON.stringify(page).includes('user_id'), false)
|
|
})
|
|
|
|
test('an unpublished results table is absent rather than empty', async () => {
|
|
store.runs[0].results_published_at = null
|
|
store.participants = [{ id: 1, member_key: 'k', score: 10, rank_at: null, meta: null }]
|
|
const page = await publicModel.event('the-yew-invasion')
|
|
assert.equal(page.event.results, null)
|
|
})
|
|
|
|
test('?run= selects which occurrence the results are about', async () => {
|
|
const older = {
|
|
...run({ id: 3600, scheduled_for: new Date('2026-08-22T00:00:00Z') }),
|
|
definition_title: 'The Yew Invasion',
|
|
definition_slug: 'the-yew-invasion',
|
|
}
|
|
store.runs.push(older)
|
|
const page = await publicModel.event('the-yew-invasion', { runId: '3600' })
|
|
assert.equal(page.event.results.runId, 3600)
|
|
})
|
|
|
|
test('a run id belonging to no occurrence of this event renders the page anyway', async () => {
|
|
// A stale link in a months-old mail should land on the event it was about, not
|
|
// on a dead end.
|
|
const page = await publicModel.event('the-yew-invasion', { runId: '999999' })
|
|
assert.equal(page.ok, true)
|
|
assert.equal(page.event.slug, 'the-yew-invasion')
|
|
})
|
|
|
|
// ── The arc ────────────────────────────────────────────────────────────────
|
|
|
|
test('a series with nothing listed in it is a 404, not an empty page', async () => {
|
|
store.series = [{ id: 5, name: 'The Yew Campaign', slug: 'the-yew-campaign', description: null }]
|
|
store.definitions[0].series_id = 5
|
|
store.definitions[0].listed = false
|
|
const arc = await publicModel.series('the-yew-campaign')
|
|
assert.equal(arc.ok, false)
|
|
assert.equal(arc.status, 404)
|
|
})
|
|
|
|
test('an arc lists its listed events and nothing about their plans', async () => {
|
|
store.series = [{ id: 5, name: 'The Yew Campaign', slug: 'the-yew-campaign', description: 'An arc.' }]
|
|
store.definitions[0].series_id = 5
|
|
const arc = await publicModel.series('the-yew-campaign')
|
|
assert.equal(arc.ok, true)
|
|
assert.deepEqual(Object.keys(arc.series.events[0]).sort(), ['imageUrl', 'slug', 'summary', 'title'])
|
|
})
|
|
|
|
// ── Participation history ──────────────────────────────────────────────────
|
|
|
|
test('history publishes the rank as null until results were published', async () => {
|
|
store.history = [
|
|
{
|
|
id: 9,
|
|
run_id: 3692,
|
|
score: 1420,
|
|
rank_at: null,
|
|
joined_at: new Date(),
|
|
meta: null,
|
|
scheduled_for: new Date('2026-08-29T00:00:00Z'),
|
|
started_at: null,
|
|
ended_at: null,
|
|
status: 'completed',
|
|
scope: '',
|
|
timezone: 'UTC',
|
|
results_published_at: null,
|
|
definition_title: 'The Yew Invasion',
|
|
definition_slug: 'the-yew-invasion',
|
|
series_name: null,
|
|
series_slug: null,
|
|
},
|
|
]
|
|
const result = await publicModel.history(7)
|
|
assert.equal(result.entries[0].rank, null)
|
|
assert.equal(result.entries[0].resultsPublishedAt, null)
|
|
assert.equal(result.entries[0].score, 1420)
|
|
})
|
|
|
|
test('history publishes no member key', async () => {
|
|
store.history = [
|
|
{
|
|
id: 9,
|
|
run_id: 3692,
|
|
member_key: 'serial:0x40001234',
|
|
score: 1,
|
|
rank_at: 1,
|
|
joined_at: new Date(),
|
|
meta: null,
|
|
scheduled_for: new Date(),
|
|
status: 'completed',
|
|
timezone: 'UTC',
|
|
definition_title: 't',
|
|
definition_slug: 's',
|
|
},
|
|
]
|
|
const result = await publicModel.history(7)
|
|
assert.equal(JSON.stringify(result).includes('0x40001234'), false)
|
|
})
|