~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
355 lines · 13.4 KB Raw History
// The screenshots without a PDS: what knotless-serve answered for each page,
// recorded once in fixtures.json, and played back by a stand-in for it in
// front of the pages. The pages are drawn as they are by the real app, from
// web/build; only their data is fixed.
//
//   node harness/shots/fixtures.mjs    record them again, from the world
//                                      `just shots-up` keeps up, or a new
//                                      one: `just shots-record`

import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { createServer, request } from 'node:http'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import config from './playwright.config.mjs'
import {
  base,
  create,
  now,
  pages,
  port,
  root,
  saved,
  serve,
  until,
  viewers,
  zoekt,
} from './world.mjs'

export const fixtures = join(root, 'harness/shots/fixtures.json')

/// What knotless-serve answers itself, rather than handing to the pages.
const answered = (path) => /^\/xrpc\//.test(path) || /^\/[^/]+\/[^/]+\/raw\//.test(path)

/// The headers of an answer worth keeping.
const KEPT = ['content-type', 'cache-control', 'location', 'content-security-policy']

/// knotless-serve's place on `port`, in front of the pages (from `build`,
/// else this checkout's): answering from `recorded`, or, given `from`,
/// asking knotless-serve there and recording what it says. Returns the
/// recording, and how to stop.
export async function front({ recorded = {}, from, build } = {}) {
  const drawn = pages(port + 1, base, build)
  const answer = async (path) => {
    if (!from) return recorded[path]
    const res = await fetch(from + path, { redirect: 'manual' })
    const body = Buffer.from(await res.arrayBuffer())
    const text = body.toString('utf8')
    const binary = !Buffer.from(text, 'utf8').equals(body)
    const headers = Object.fromEntries(
      KEPT.filter((h) => res.headers.has(h)).map((h) => [h, res.headers.get(h)]),
    )
    return (recorded[path] = {
      status: res.status,
      headers,
      ...(binary ? { base64: body.toString('base64') } : { body: text }),
    })
  }
  // Who a page asked as `signedIn(handle)` is: that account, as its
  // recorded profile has it, logged in, with nothing unread.
  const viewing = async (url, handle) => {
    const json = (body) => ({
      status: 200,
      headers: { 'content-type': 'application/json', 'cache-control': 'no-cache' },
      body: JSON.stringify(body),
    })
    if (url === '/xrpc/dev.knotless.getUnreadCount') return json({ count: 0 })
    // What `just look --set` answers instead, when it says.
    if (url.startsWith('/xrpc/dev.knotless.listNotifications'))
      return (await answer(url).catch(() => null)) ?? json(await heard(handle))
    if (url.startsWith('/xrpc/dev.knotless.listSecrets')) {
      const recorded = await answer(url).catch(() => null)
      return recorded?.status === 200 ? recorded : json(await kept(url))
    }
    // A repository and a pull or an issue, with what the account has to do
    // with it, which the server answers anyone logged in: nothing chosen,
    // so it hears of what it takes part in. Of a pull or an issue, its
    // author may close it, and the repository's owner close and hide it.
    if (
      url.startsWith('/xrpc/dev.knotless.getRepo?') ||
      url.startsWith('/xrpc/dev.knotless.getThread?')
    ) {
      const recorded = await answer(url).catch(() => null)
      if (recorded?.status !== 200) return null
      const body = JSON.parse(recorded.body)
      if (url.startsWith('/xrpc/dev.knotless.getRepo?'))
        return { ...recorded, body: JSON.stringify({ viewer: {}, ...body }) }
      const profile = await answer(
        `/xrpc/dev.knotless.getProfile?actor=${encodeURIComponent(handle)}`,
      )
      const { did } = JSON.parse(profile.body)
      const owner = new URLSearchParams(url.split('?')[1])
        .get('repo')
        ?.startsWith(`at://${handle}/`)
      const author = body.item?.author?.did === did
      const viewer = author
        ? { setState: true, notifiedFor: 'author' }
        : owner
          ? { setState: true, hide: true }
          : {}
      return { ...recorded, body: JSON.stringify({ viewer, ...body }) }
    }
    if (url !== '/xrpc/dev.knotless.getViewer') return null
    const profile = await answer(
      `/xrpc/dev.knotless.getProfile?actor=${encodeURIComponent(handle)}`,
    )
    if (!profile) return null
    const { did, displayName, avatar } = JSON.parse(profile.body)
    // Over what was recorded for no one, so what `just look --set` changes holds.
    const recorded = await answer(url).catch(() => null)
    const base = recorded?.status === 200 ? JSON.parse(recorded.body) : {}
    return json({ ...base, actor: { did, handle, displayName, avatar }, login: true })
  }
  // The secrets an owner keeps for the tally, made from the recording, as
  // they are asked for logged in, which the recording never is: two sealed
  // to its runner's key, one for main's pushes alone.
  const kept = async (url) => {
    const runners = JSON.parse(
      (
        await answer(
          `/xrpc/dev.knotless.listRunners?repo=${encodeURIComponent('at://alice.test/dev.knotless.repo/tally')}`,
        )
      ).body,
    )
    const [{ runner, key }] = runners.keys
    const secret = (name, refs, updatedAt) => ({
      name,
      runner,
      ...(refs ? { refs: refs.map((pattern) => ({ pattern })) } : {}),
      key,
      updatedAt,
    })
    return {
      secrets: [
        secret('DEPLOY_TOKEN', ['refs/heads/main'], '2026-10-25T17:52:00Z'),
        secret('NPM_TOKEN', null, '2026-10-26T09:10:00Z'),
      ],
    }
  }
  // What `handle` heard of, made from the recording, as no index there
  // keeps any for a recorded login: bob.test's doings on alice.test's
  // tally, each reason, its checks failing and ejected, and an invitation to bob.test as an organization,
  // the newest three not yet seen.
  const heard = async (handle) => {
    const read = async (path) => JSON.parse((await answer(path)).body)
    const owner = await read(`/xrpc/dev.knotless.getProfile?actor=${encodeURIComponent(handle)}`)
    const bob = await read('/xrpc/dev.knotless.getProfile?actor=bob.test')
    const items = (kind) =>
      read(
        `/xrpc/dev.knotless.listItems?repo=${encodeURIComponent(`at://${handle}/dev.knotless.repo/tally`)}&kind=${kind}`,
      )
    const [issue] = (await items('issue')).items
    const [pull] = (await items('pull')).items
    const author = {
      did: bob.did,
      handle: bob.handle,
      ...(bob.avatar ? { avatar: bob.avatar } : {}),
    }
    const repo = { did: owner.did, handle, name: 'tally' }
    const at = (hours) => new Date(Date.parse(now) - hours * 3600_000).toISOString()
    const comment = `at://${bob.did}/dev.knotless.comment/3myvwhm3ntion`
    const notifications = [
      {
        uri: comment,
        reason: 'mention',
        reasonSubject: issue.uri,
        title: issue.title,
        text: '@alice.test can you look at this one? It fails on **every** file with a BOM.',
        isRead: false,
        createdAt: at(1),
      },
      {
        uri: `at://${bob.did}/dev.knotless.org/self`,
        reason: 'invite',
        role: 'member',
        isRead: false,
        createdAt: at(2),
      },
      { uri: pull.uri, reason: 'pull', title: pull.title, isRead: false, createdAt: at(3) },
      {
        uri: `at://${bob.did}/dev.knotless.comment/3myvwhc0mment`,
        reason: 'comment',
        reasonSubject: issue.uri,
        title: issue.title,
        text: 'Same here on 0.4.2.',
        isRead: true,
        createdAt: at(26),
      },
      {
        uri: `at://${bob.did}/dev.knotless.check/3myvwhch3cks`,
        reason: 'checks-failed',
        reasonSubject: pull.uri,
        title: pull.title,
        checks: [
          { workflow: 'test', status: 'failure' },
          { workflow: 'web', status: 'error' },
        ],
        isRead: true,
        createdAt: at(30),
      },
      {
        uri: `at://${bob.did}/dev.knotless.queue/3myvwhqu3ue`,
        reason: 'ejected',
        reasonSubject: pull.uri,
        title: pull.title,
        checks: [{ workflow: 'shots', status: 'failure' }],
        isRead: true,
        createdAt: at(40),
      },
      { uri: issue.uri, reason: 'mention', title: issue.title, isRead: true, createdAt: at(50) },
      {
        uri: `at://${bob.did}/dev.knotless.star/3myvwhst4rred`,
        reason: 'star',
        isRead: true,
        createdAt: at(74),
      },
      {
        uri: `at://${bob.did}/dev.knotless.follow/3myvwhf0llow`,
        reason: 'follow',
        isRead: true,
        createdAt: at(98),
      },
    ].map((n) => ({
      ...n,
      author,
      ...(n.reason === 'follow' || n.reason === 'invite' ? {} : { repo }),
    }))
    return { notifications, seenAt: at(4) }
  }
  const server = createServer(async (req, res) => {
    if (answered(req.url)) {
      const as = req.headers.cookie?.match(/(?:^|;\s*)shots_as=([^;]+)/)?.[1]
      // A choice of what to hear of, as the server keeps it, for what one
      // signed in clicks to look at.
      if (as && req.url === '/xrpc/dev.knotless.putSubscription') {
        let input = ''
        for await (const chunk of req) input += chunk
        res.writeHead(200, { 'content-type': 'application/json' })
        res.end(JSON.stringify({ ...JSON.parse(input), createdAt: now }))
        return
      }
      const a =
        (as && (await viewing(req.url, decodeURIComponent(as)))) ||
        (await answer(req.url).catch(() => null))
      if (!a) {
        res.writeHead(404, { 'content-type': 'application/json' })
        res.end(JSON.stringify({ message: `nothing recorded for ${req.url}` }))
        return
      }
      res.writeHead(a.status, a.headers)
      res.end(a.base64 === undefined ? a.body : Buffer.from(a.base64, 'base64'))
      return
    }
    const headers = {
      ...req.headers,
      'x-forwarded-host': req.headers.host,
      'x-forwarded-proto': 'http',
    }
    const on = request(
      { host: '127.0.0.1', port: port + 1, path: req.url, method: req.method, headers },
      (up) => {
        res.writeHead(up.statusCode, up.headers)
        up.pipe(res)
      },
    )
    on.on('error', (e) => {
      res.writeHead(502)
      res.end(`The pages are not being drawn: ${e.message}`)
    })
    req.pipe(on)
  })
  await new Promise((r) => server.listen(port, '127.0.0.1', r))
  const kill = () => {
    server.close()
    server.closeAllConnections()
    drawn.kill()
    // Gone once both have let go of their ports, for the next to take them.
    const exited = drawn.exitCode !== null || drawn.signalCode !== null
    return Promise.all([
      new Promise((r) => server.once('close', r)),
      exited ? null : new Promise((r) => drawn.once('exit', r)),
    ])
  }
  try {
    await until('the pages', async () => (await fetch(`${base}/start`)).ok)
  } catch (e) {
    kill()
    throw e
  }
  return { recorded, kill }
}

/// The cookies a page is asked with: the zone the browser has said
/// (views.spec.mjs), and for a view in `viewers`, who it is signed in as,
/// which front() answers getViewer with.
export const cookies = (name) =>
  [
    `tz=${encodeURIComponent(config.use.timezoneId)}`,
    ...(viewers[name] ? [signedIn(viewers[name])] : []),
  ].join('; ')

/// The cookie that signs a page in as `handle`, to front() alone.
export const signedIn = (handle) => `shots_as=${encodeURIComponent(handle)}`

/// The recording in `file`, this checkout's unless given, if there is one.
export const recording = (file = fixtures) =>
  existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null

/// Records every page's answers from `world`, and writes them with the pages.
async function record(world) {
  const at = port + 2
  const server = await serve(world, { at, alone: true })
  const stand = await front({ from: `http://127.0.0.1:${at}` })
  try {
    // Asked as the browser asks (views.spec.mjs), having said its zone and
    // signed in where the view is, so what the pages ask then is what is kept.
    for (const [name, path] of Object.entries(world.views)) {
      const html = await (await fetch(base + path, { headers: { cookie: cookies(name) } })).text()
      // What the browser asks for itself: images, from the raw files and
      // the pictures checks made.
      for (const [, src] of html.matchAll(/src="(\/[^"]+\/(?:raw|image)\/[^"]+)"/g))
        await fetch(base + src)
    }
  } finally {
    stand.kill()
    server.kill()
  }
  const sorted = Object.fromEntries(
    Object.entries(stand.recorded).sort(([a], [b]) => (a < b ? -1 : 1)),
  )
  writeFileSync(fixtures, JSON.stringify({ views: world.views, answers: sorted }, null, 1) + '\n')
  return Object.keys(sorted).length
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  if (!zoekt) {
    console.error(
      'zoekt-webserver and zoekt-git-index are not on PATH: the code views record what they find (see the zoekt step in .knotless/workflows/test.yml)',
    )
    process.exit(1)
  }
  const kept = existsSync(saved) ? JSON.parse(readFileSync(saved, 'utf8')) : null
  const up =
    kept &&
    (await fetch(`${kept.net.pds}/xrpc/com.atproto.server.describeServer`).then(
      (r) => r.ok,
      () => false,
    ))
  const world = up ? kept : await create()
  // A new world's server is stopped first: the recording starts its own.
  if (!up) world.server.kill()
  const n = await record(world)
  if (!up) world.stop()
  console.log(`recorded ${n} answers for ${Object.keys(world.views).length} pages in ${fixtures}`)
  process.exit(0)
}