Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 84 additions & 43 deletions packages/zulip/contract.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
* (`/register`) — a reused adapter re-polls a stale queue that silently
* stops delivering. The bot account is suite-scoped; only the adapter is
* per-test.
* - **Per-test channel namespace.** `seedChannel(name)` creates a real stream
* named `cc-ct-<run>-<seq>-<name>` so each test reads/writes an isolated
* channel. The contract reuses fixed logical names (`lobby`, `alpha`) across
* ~50 tests; on a realm that persists channel state that is cross-test
* contamination within a run (the channel-wide `readChannel` range tests
* assert `not.toContain` / exact-equality). The fake sidesteps it by
* building a fresh realm per `beforeEach`; the live factory namespaces
* instead. Streams accumulate (the non-admin minter cannot delete them) —
* acceptable on a test realm.
* - **A fixed pool of channels, emptied before each use.** Zulip's API cannot
* delete a channel, and an archived one still loads in every web client, so
* a run must create none. `seedChannel(name)` maps each logical name the
* contract uses (`lobby`, `alpha`, …) to one permanent stream,
* `cc-ct-<name>`, created by the first run ever and reused by every run
* after. The contract's channel-wide reads assert exact contents, so the
* first seed of a name in a test empties that stream: it clears the
* description and moves every topic out to `cc-ct-spent`. The minter created
* both streams, so the realm makes it their administrator, which lets it
* move topics out. Emptying before use rather than after covers a run that
* failed or was killed. The fake sidesteps all this by building a fresh
* realm per `beforeEach`.
* - **Shared peer agents.** `seedAgent(name)` find-or-creates a persistent
* peer bot (`cc-contract-peer-<name>`) once and reuses it — peers carry no
* per-test state, so suite-level reuse costs no per-test mint.
Expand Down Expand Up @@ -91,13 +94,21 @@ const httpClient = Effect.runSync(HttpClient.HttpClient.pipe(Effect.provide(Fetc
const MINTER_PACE = Duration.millis(900)

const SELF_NAME = decodeBotNameSync('cc-contract-self')
const NS_PREFIX = 'cc-ct'
// ponytail: one pool per realm, so two runs at once on the same realm
// trample each other's channels; namespace the pool per machine if that happens
const POOL_PREFIX = 'cc-ct'
const SPENT_CHANNEL = `${POOL_PREFIX}-spent`
const PEER_PREFIX = 'cc-contract-peer'

const successSchema = Schema.Struct({ result: Schema.Literal('success') })
const streamsListSchema = Schema.Struct({
const streamIdSchema = Schema.Struct({ result: Schema.Literal('success'), stream_id: Schema.Int })
const streamSchema = Schema.Struct({
result: Schema.Literal('success'),
streams: Schema.Array(Schema.Struct({ name: Schema.NonEmptyString, stream_id: Schema.Int })),
stream: Schema.Struct({ description: Schema.String }),
})
const topicsSchema = Schema.Struct({
result: Schema.Literal('success'),
topics: Schema.Array(Schema.Struct({ max_id: Schema.Int })),
})
const usersListSchema = Schema.Struct({
result: Schema.Literal('success'),
Expand Down Expand Up @@ -144,11 +155,8 @@ if (env === undefined) {
minterApiKey: Redacted.make(Effect.runSync(ApiKey(liveEnv.minterApiKey))),
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.orDie)

// One process-wide run id keeps each `bun run test:live` invocation's streams
// distinct from prior runs; the per-call sequence keeps tests distinct within
// a run. (Test files may use Date.now — only Workflow scripts may not.)
const runId = Date.now().toString(36)
let seq = 0
// Pool stream ids never change, so each is looked up once per run.
const poolIds = new Map<string, number>()

// Persistent peer bots are find-or-created once and cached for the suite.
const peerCache = new Map<string, Identity>()
Expand Down Expand Up @@ -178,34 +186,59 @@ if (env === undefined) {
// event pump is unwound here so handles don't leak across the run.
const builtAdapters: ZulipAdapter[] = []

const ensureStream = (minter: ZulipHttp, realName: string): Effect.Effect<ChannelRef> =>
minter
.post('/users/me/subscriptions', successSchema, {
// Subscribing creates the stream only the first time any run asks for it.
const poolStream = (minter: ZulipHttp, realName: string) =>
Effect.gen(function* () {
const cached = poolIds.get(realName)
if (cached !== undefined) return cached
yield* minter.post('/users/me/subscriptions', successSchema, {
subscriptions: JSON.stringify([{ name: realName }]),
})
.pipe(
Effect.zipRight(
minter.get('/streams', streamsListSchema, {
include_public: true,
include_subscribed: true,
const { stream_id } = yield* minter.get('/get_stream_id', streamIdSchema, {
stream: realName,
})
poolIds.set(realName, stream_id)
return stream_id
})

// A minter that is not a moderator may move a message only within the
// realm's move time limit, so a stream left unemptied for longer than that
// fails here until a realm administrator empties it.
const emptyStream = (minter: ZulipHttp, streamId: number) =>
Effect.gen(function* () {
const spentId = yield* poolStream(minter, SPENT_CHANNEL)
const { stream } = yield* minter.get(`/streams/${streamId}`, streamSchema)
// Clearing a description posts a notice into the stream, so this goes
// before the topics are moved out.
if (stream.description !== '')
yield* minter.patch(`/streams/${streamId}`, successSchema, { description: '' })
const { topics } = yield* minter.get(`/users/me/${streamId}/topics`, topicsSchema)
yield* Effect.forEach(
topics,
(topic) =>
minter.patch(`/messages/${topic.max_id}`, successSchema, {
stream_id: spentId,
propagate_mode: 'change_all',
send_notification_to_old_thread: false,
send_notification_to_new_thread: false,
}),
),
Effect.flatMap((res) => {
const found = res.streams.find((s) => s.name === realName)
if (found === undefined)
return Effect.die(new Error(`seedChannel: stream ${realName} not found after create`))
return Effect.succeed<ChannelRef>(
withChannelPermalink(
permalinkBase({ realmUrl: Effect.runSync(RealmUrl(liveEnv.site)) }),
{
id: decodeChannelIdSync(String(found.stream_id)),
name: decodeChannelNameSync(realName),
},
),
)
}),
Effect.orDie,
{ discard: true },
)
})

const seedPoolChannel = (minter: ZulipHttp, name: string): Effect.Effect<ChannelRef> => {
const realName = `${POOL_PREFIX}-${name}`
return poolStream(minter, realName).pipe(
Effect.tap((streamId) => emptyStream(minter, streamId)),
Effect.map((streamId) =>
withChannelPermalink(permalinkBase({ realmUrl: Effect.runSync(RealmUrl(liveEnv.site)) }), {
id: decodeChannelIdSync(String(streamId)),
name: decodeChannelNameSync(realName),
}),
),
Effect.orDie,
)
}

const ensurePeer = (
minter: ZulipHttp,
Expand Down Expand Up @@ -241,7 +274,6 @@ if (env === undefined) {
})

const liveContractFactory: AttachmentContractFactory = async () => {
const ns = `${NS_PREFIX}-${runId}-${seq++}`
const adapter = await Effect.runPromise(buildAdapter())
builtAdapters.push(adapter)
const minter = await Effect.runPromise(buildMinterHttp())
Expand All @@ -255,10 +287,19 @@ if (env === undefined) {
for (const channel of held)
await Effect.runPromise(adapter.inbox.unsubscribe(channel).pipe(Effect.orDie))

// A name seeded twice in one test is the same channel, emptied only once.
const seeded = new Map<string, ChannelRef>()

return {
comms: adapter,
attachments: adapter,
seedChannel: (name) => ensureStream(minter, `${ns}-${name}`),
seedChannel: (name) => {
const cached = seeded.get(name)
if (cached !== undefined) return Effect.succeed(cached)
return seedPoolChannel(minter, name).pipe(
Effect.tap((channel) => Effect.sync(() => seeded.set(name, channel))),
)
},
seedAgent: (name) => ensurePeer(minter, adapter, name),
// A seeded peer posts (mentioning self) through its own bound adapter so
// the mention-floor tests run cross-identity — the shape that surfaces on
Expand Down