Public: anyone can read this; the crew can change it.
Feed ranking ignores archived repositories when it works out what the viewer cares about #3485
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3478-feed-ranking-ignores-archived-repositori → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +70
−1
src/feed/affinity.ts +4 −1
| @@ -124,7 +124,10 @@ export function affinityFor( | ||
| 124 | 124 | now: number, |
| 125 | 125 | ): Affinity { |
| 126 | 126 | const aff = new Affinity(); |
| 127 | const mine = listReposWritableTo(db, viewer, { limit: 200 }); | |
| 127 | const mine = listReposWritableTo(db, viewer, { | |
| 128 | limit: 200, | |
| 129 | withoutArchived: true, | |
| 130 | }); | |
| 128 | 131 | for (const repo of mine) |
| 129 | 132 | aff.add( |
| 130 | 133 | `app:${repo.id}`, |
test/feed-affinity-archived.test.ts added +66
| @@ -0,0 +1,66 @@ | ||
| 1 | /** An archived repository stops raising the viewer's affinity; waking it brings it back. */ | |
| 2 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { moveArchived, type Repo } from "../src/core/repos.ts"; | |
| 9 | import { createUser, type User } from "../src/core/users.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | import { affinityFor } from "../src/feed/affinity.ts"; | |
| 12 | ||
| 13 | describe("feed affinity and archive", () => { | |
| 14 | let dataDir: string; | |
| 15 | let app: App; | |
| 16 | let servers: Servers; | |
| 17 | let alice: User; | |
| 18 | let old: Repo; | |
| 19 | let live: Repo; | |
| 20 | const NOW = Date.parse("2026-01-01T00:00:00Z"); | |
| 21 | ||
| 22 | beforeAll(async () => { | |
| 23 | dataDir = await mkdtemp(join(tmpdir(), "forge-affinity-archived-")); | |
| 24 | app = createApp({ dataDir, port: 0 }); | |
| 25 | servers = await serve(app); | |
| 26 | const r = await createUser(app.db, { | |
| 27 | origin: "seed", | |
| 28 | trusted: true, | |
| 29 | username: "alice", | |
| 30 | allowReserved: true, | |
| 31 | email: "[email protected]", | |
| 32 | password: "a-good-passphrase", | |
| 33 | adminIfFirst: true, | |
| 34 | }); | |
| 35 | if (!r.ok) throw new Error(r.error); | |
| 36 | alice = r.value; | |
| 37 | const mk = async (name: string) => { | |
| 38 | const c = await createRepo(app, alice, { name, visibility: "public" }); | |
| 39 | if (!c.ok) throw new Error(`repo: ${JSON.stringify(c)}`); | |
| 40 | return c.value; | |
| 41 | }; | |
| 42 | old = await mk("old"); | |
| 43 | live = await mk("live"); | |
| 44 | }); | |
| 45 | afterAll(async () => { | |
| 46 | servers.stop(); | |
| 47 | app.close(); | |
| 48 | await rm(dataDir, { recursive: true, force: true }); | |
| 49 | }); | |
| 50 | ||
| 51 | test("archiving drops a repository from the score, waking restores it", async () => { | |
| 52 | const score = (r: Repo) => affinityFor(app.db, alice, NOW).app(r.id).score; | |
| 53 | expect(score(old)).toBeGreaterThan(0); | |
| 54 | expect(score(live)).toBeGreaterThan(0); | |
| 55 | ||
| 56 | expect(moveArchived(app.db, old.id, true)).toBe(true); | |
| 57 | expect(score(old)).toBe(0); | |
| 58 | expect(score(live)).toBeGreaterThan(0); | |
| 59 | ||
| 60 | const page = await fetch(`http://localhost:${servers.port}/alice/old`); | |
| 61 | expect(page.status).toBe(200); | |
| 62 | ||
| 63 | expect(moveArchived(app.db, old.id, false)).toBe(true); | |
| 64 | expect(score(old)).toBeGreaterThan(0); | |
| 65 | }); | |
| 66 | }); |
new above
offline
This title claims more than the code does: a pin or a past interaction on an archived repository still scores. Narrow the name, or cover a pinned archived repository so the intended behaviour is pinned down.