diff --git a/src/lexicon/tangled.rs b/src/lexicon/tangled.rs index b6ba7c4..e14d3ca 100644 --- a/src/lexicon/tangled.rs +++ b/src/lexicon/tangled.rs @@ -443,6 +443,9 @@ mod tests { use tangled_lexicon::sh_tangled::feed::comment::Comment as FeedComment; use tangled_lexicon::sh_tangled::public_key::PublicKey; use tangled_lexicon::sh_tangled::repo::Repo; + use tangled_lexicon::sh_tangled::repo::issue::Issue; + use tangled_lexicon::sh_tangled::repo::issue::comment::Comment as LegacyIssueComment; + use tangled_lexicon::sh_tangled::repo::issue::state::State as IssueStateRecord; use tangled_lexicon::sh_tangled::repo::pull::Pull; use tangled_lexicon::sh_tangled::repo::pull::status::Status as PullStatus; @@ -608,5 +611,234 @@ mod tests { round_trips::(&value); } } + + // ------------------------------------------------------------------- + // The issue family + // ------------------------------------------------------------------- + + /// A modern `sh.tangled.repo.issue`, in the shape every current + /// writer produces: `repo` a DID, `createdAt` a UTC stamp, and + /// `mentions`/`references` present and empty rather than omitted. + #[test] + fn a_current_issue_record_parses_and_round_trips() { + round_trips::(&single(fixture!("issue_new_record.json"))); + } + + /// The record `Issue` cannot read, and the reason `cmd::issue::read` + /// reads issues off `Value`. + /// + /// Three refusals in one captured record, any one of which is + /// enough: `repo` holds the pre-DID at-uri spelling where the schema + /// says `did`, `createdAt` is the empty string where the schema says + /// `datetime`, and `owner`/`issueId` are properties the lexicon has + /// never had. `issue list`, `issue view` and `issue close` all still + /// work on it — see `cmd::issue::read`'s + /// `the_shapes_a_live_issue_collection_actually_holds`, which + /// asserts what each read helper answers for this same record. + #[test] + fn a_pre_did_issue_record_is_refused_by_the_generated_type() { + let value = single(fixture!("issue_old_record.json")); + assert!( + value["repo"] + .as_str() + .is_some_and(|r| r.starts_with("at://")), + "fixture names its repo by at-uri" + ); + assert_eq!(value["createdAt"], "", "fixture has an empty stamp"); + assert!(value.get("issueId").is_some(), "fixture carries a number"); + let Err(refusal) = parses::(&value) else { + panic!( + "`Issue` now accepts a pre-DID record. If the schema relaxed \ + `repo`/`createdAt`, this test and `cmd::issue::read`'s permissive \ + field reads can both be revisited." + ); + }; + assert!(!refusal.is_empty()); + } + + /// One captured page of one account's issue collection, which is what + /// `issue list` reads: six records the generated type accepts and + /// four it refuses, in the order the PDS returned them. + /// + /// The mix is the point. Every row on this page is an issue somebody + /// filed and the appview shows, so a listing that dropped the four + /// would be missing rows rather than refusing to run. + #[test] + fn a_live_issue_collection_holds_shapes_the_generated_type_refuses() { + let (mut ok, mut refused) = (0, 0); + for value in values(fixture!("pds_issues_page.json"), "records") { + match parses::(&value) { + Ok(()) => { + round_trips::(&value); + ok += 1; + } + Err(_) => refused += 1, + } + } + assert_eq!((ok, refused), (6, 4), "the captured page's mix changed"); + } + + /// The index does *not* re-emit an issue record the way it re-emits a + /// pull one. + /// + /// `sh.tangled.repo.listIssuesBy` hands back a `value` that has been + /// rewritten: a pre-DID record's `repo` at-uri is replaced with the + /// repo's DID and the record's own `repoDid` property is dropped, + /// under the record's real CID. So every item on this page parses as + /// `Issue` while the record it stands for does not, and the two + /// disagree byte for byte — which is why `cmd::issue::read::Listed` + /// reads its fields off whichever row survived the merge instead of + /// treating the two as interchangeable. Contrast + /// [`the_indexed_copy_of_a_pull_is_the_same_record`]. + #[test] + fn the_indexed_copy_of_an_issue_is_not_always_the_record() { + let pds: Value = + serde_json::from_str(fixture!("pds_issues_page.json")).expect("fixture is JSON"); + let records: Vec = pds["records"].as_array().expect("records").clone(); + + let mut rewritten = 0; + for item in values(fixture!("bobbin_list_issues_by.json"), "items") { + // Every indexed item parses, whatever the record behind it + // looks like. + assert!(parses::(&item).is_ok(), "an indexed item refused"); + } + let page: Value = + serde_json::from_str(fixture!("bobbin_list_issues_by.json")).expect("JSON"); + for item in page["items"].as_array().expect("items") { + let Some(record) = records.iter().find(|r| r["uri"] == item["uri"]) else { + continue; + }; + if record["value"] == item["value"] { + continue; + } + rewritten += 1; + assert_eq!( + record["cid"], item["cid"], + "the index changed the bytes and kept the CID; that is the finding" + ); + assert!( + record["value"]["repo"] + .as_str() + .is_some_and(|r| r.starts_with("at://")), + "the record names its repo by at-uri" + ); + assert_eq!( + item["value"]["repo"], record["value"]["repoDid"], + "the index substituted the record's own repoDid" + ); + assert!(parses::(&record["value"]).is_err()); + } + assert!(rewritten > 0, "the captured overlap no longer disagrees"); + } + + /// State records, the log `issue view` settles a state out of. + /// + /// Two of the twelve on this captured page are neither dated nor + /// pointed at anything — `createdAt` absent and `issue` the empty + /// string, both required — so `State` refuses them while + /// `cmd::issue::read`'s `state_events` reads them and lets them lose + /// on the sort. The other page is a live close/reopen/close chain + /// three seconds wide, every record of it well formed. + #[test] + fn a_live_state_log_holds_records_the_generated_type_refuses() { + let (mut ok, mut refused) = (0, 0); + for value in values(fixture!("issue_states.json"), "records") { + match parses::(&value) { + Ok(()) => { + round_trips::(&value); + ok += 1; + } + Err(_) => refused += 1, + } + } + assert_eq!((ok, refused), (10, 2), "the captured page's mix changed"); + + for value in values(fixture!("issue_states_reopened.json"), "records") { + round_trips::(&value); + } + } + + /// The legacy comment collection, which has bytes behind it now — + /// and the second place the index hands back something that is not + /// the record. + /// + /// `sh.tangled.repo.issue.comment` is deprecated for writing and is + /// still the whole of any thread older than the unification. The + /// records themselves parse as their own generated type and never + /// as `FeedComment`. What `sh.tangled.feed.listComments` returns for + /// the same records, under the same CIDs, is a different thing + /// again: the index normalises the legacy shape towards the current + /// one — `body` becomes an object with a `text`, the bare `issue` + /// at-uri becomes a `subject` strongRef whose `cid` is `bafkqaaa`, + /// the CID of no bytes at all — while leaving `$type` naming the + /// legacy collection. So the index copy no longer parses as the + /// record's own type, and does parse as `FeedComment`, tag and all. + /// + /// That is what `clients::tangled::comments` reads a thread off + /// `Value` for, and it is why it reads two body spellings: one is + /// the record's, the other is the index's rewrite of it. + #[test] + fn a_legacy_issue_comment_is_not_the_record_the_index_hands_back() { + for value in values(fixture!("issue_comment_records.json"), "records") { + round_trips::(&value); + assert!( + value["body"].is_string(), + "the record spells its body as a plain string" + ); + assert!( + parses::(&value).is_err(), + "the legacy shape must not read as a feed comment" + ); + } + + let thread = values(fixture!("feed_comments_on_issue.json"), "items"); + assert_eq!(thread.len(), 3, "the captured thread changed"); + for value in &thread { + assert_eq!(value["$type"], "sh.tangled.repo.issue.comment"); + assert!( + parses::(value).is_err(), + "no longer its own type, once the index has rewritten it" + ); + // And it now reads as the *current* type, though `$type` + // still names the legacy collection: the generated structs + // do not check their own tag on the way in. Reserializing + // one emits `$type` twice — the container's tag and the one + // `extra_data` carried in — so the round trip is not + // lossless even though the parse succeeds. + assert!(parses::(value).is_ok()); + assert!( + value["body"]["text"].is_string(), + "body was lifted into an object" + ); + assert_eq!( + value["subject"]["cid"], "bafkqaaa", + "the strongRef the index invents carries the CID of no bytes" + ); + } + + // The same record on both sides, under the same CID, is what + // makes the rewrite a fact rather than a guess about two + // different captures. + let records: Value = + serde_json::from_str(fixture!("issue_comment_records.json")).expect("JSON"); + let items: Value = + serde_json::from_str(fixture!("feed_comments_on_issue.json")).expect("JSON"); + let mut paired = 0; + for item in items["items"].as_array().expect("items") { + let Some(record) = records["records"] + .as_array() + .expect("records") + .iter() + .find(|r| r["uri"] == item["uri"]) + else { + continue; + }; + paired += 1; + assert_eq!(record["cid"], item["cid"]); + assert_eq!(record["value"]["body"], item["value"]["body"]["text"]); + assert_eq!(record["value"]["issue"], item["value"]["subject"]["uri"]); + } + assert_eq!(paired, 1, "the captured overlap changed"); + } } } diff --git a/tests/fixtures/bobbin_list_issues_by.json b/tests/fixtures/bobbin_list_issues_by.json new file mode 100644 index 0000000..675ddb8 --- /dev/null +++ b/tests/fixtures/bobbin_list_issues_by.json @@ -0,0 +1,152 @@ +{ + "items": [ + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mne7vysjvq22", + "cid": "bafyreicdeyll5u53fjdgnrwznf2pbocag3umh73vnailapk6fgro5elm7u", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "Repositories can currently be created without a default branch, causing the repository index page to appear empty. Enforce a default branch for all repositories to ensure repository content is accessible and the index page functions correctly.\n\nFor Knot, this means it will fallback to some branch when no configured default branch is not found.\n\nFor XRPC lexicon, this means `defaultBranch` will be a required field in output schema.\n\nFor AppView/KnotMirror, no changes yet. Wait until most repos to have a default branch for backwards compatibility.", + "createdAt": "2026-06-03T07:00:29+03:00", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "make default branch required for repositories" + }, + "state": "open", + "commentCount": 3 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mn7pow5pwr22", + "cid": "bafyreiarzdwbn6k74sxorxkyfjq3tby5oqwpsnclwq2cjthrjyhrdlunxa", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "Syncing is pretty complicated task. We should consider backfilling, event orders and all sort of stuffs. If it's unnecessary, it's better to avoid it. As I mentioned from my [Knot-stored COB proposal](https://leaflet.pub/p/did:plc:xasnlahkri4ewmbuzly2rlc5/3mmex6biynk2g), Spindle is more closer to BlueSky Feed Generator than just Data Server.\n\n- We don't globally index pipelines.\n- We are fine with pipelines tied to the spindle instance.\\\n \"Owning pipelines\" or \"Migrating pipelines between spindles\" doesn't make much sense. You should own the automation part, not result part.\n\nThus, we don't need sync for pipelines and then it is way simpler to just fetch pipelines on-demand like Bluesky Feeds. AppView doesn't have to know the real-time status of each pipelines and even if we do need it in future (e.g. AppView-side notification,) syncing will be completely optional and won't be a core requirement for running the infra.\n\n## Implementation\n\nspindle will expose two APIs to fetch pipelines:\n\n- `org.tangled.ci.listPipelines(repo DID, commits []OID)` - receive repo DID & commit OIDs and return list of pipeline statuses associated to each commits.\n- `org.tangled.ci.subscribePipelineLogs(pipeline string)` - receive pipeline ID and stream real-time logs. similar to current `/logs` endpoint.\n\nOn page render, AppView will lazy-load pipeline statuses. In this style, it becomes way easier to periodically update each pipeline statuses in real-time.\n\ntl;dr\n- pipelines are similar to feedgen, so we don't have to sync pipelines\n- spindle event stream will be optional and will be removed for simplicity", + "createdAt": "2026-06-01T11:59:33+03:00", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "proposal: deprecate spindlestream, fetch pipeline statuses on-demand" + }, + "state": "closed", + "stateUpdatedAt": "2026-07-26T12:57:39.517134Z", + "commentCount": 2 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mmqsqd7qom22", + "cid": "bafyreih2muidkqaof4lqxg37242yhw5qmflwmsktgbbgtontqpu5c5imkm", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "ttt", + "createdAt": "2026-05-26T13:44:05+03:00", + "repo": "did:plc:uthf4ondbbjclgkltftotil6", + "title": "testing" + }, + "state": "open", + "commentCount": 1 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mmbnhtrah522", + "cid": "bafyreiavxbyqvxu6ikxfvuinhayqm5k33kawp7xd3azevboun3vt4ajefa", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "example: https://tangled.org/tangled.org/core/pulls/1459/round/1\n\nmergeCheck only shows the first merge conflict of the PR stack and not all. This is not an expected behavior.", + "createdAt": "2026-05-20T12:59:56+03:00", + "references": [ + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mlehfetyvv22" + ], + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "mergeCheck doesn't show all conflicts" + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlzxvsoidf22", + "cid": "bafyreig5yxy7br5lcu6eodyehchlwrfcxaevcc2x77gp7256hsvcuo5oma", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "For example in PR compose page, label checkboxes should not be interactive when form is submitted. [`hx-disabled-elt`](https://htmx.org/attributes/hx-disabled-elt/) can be used for this.", + "createdAt": "2026-05-17T11:45:24+03:00", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "disable entire form while submitting" + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlygnbdl4622", + "cid": "bafyreihsdn6d73jwbrf43hiljklhuyzbaxjqfc6m5abmdylyntepabpmkm", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "km should wait until ongoing resync to complete.\n\nreproduce steps:\n1. create super large repo \n2. km fails to sync it due to timeout\n3. see tree/index pages are rendered as empty\n4. see blob page does render ", + "createdAt": "2026-05-16T18:03:45Z", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "bug: don't respond when repository is not fully synced yet." + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlyffalt7s22", + "cid": "bafyreig56sv6segyauw6vze2okyeac5momdlrudolegn35h4k3tkvyz4ty", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "context: [discord thread](https://discord.com/channels/1361963801993285692/1361991850327674932/1505199771445493891)\n\n> what do y’all think of like, atproto-identity <-> email mapping being public?\\\n> ...\\\n> problem is we should verify that in both way. boltless should not claim that he is `lewis@example.com`\\\n> ...\\\n> it would be nice if stateless client can inherit from specific appviews.\n\nAlso related:\n- [#133 fix: allow multiple subdomains in author email addresses](https://tangled.org/tangled.org/core/issues/133)\n- [#342 Can't associate @users.noreply.github.com email to account, fails `net.LookupMX(domain)`](https://tangled.org/tangled.org/core/issues/342)\n- [#515 Allow private GitHub email address associations](https://tangled.org/tangled.org/core/issues/515)\n\nBasically any services with `sh.tangled.email.resolveEmail` and `sh.tangled.email.resolveDid` will be email verification service (usually an appview). This service is expected to perform email verification against the user in various methods.\n\nExample responses of `sh.tangled.email.resolveEmail`:\n```jsonc\n// email=alice@gmail.com\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"email_challenge\"\n}\n\n// email=alice.com@users.noreply.tangled.org\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"service_assigned\"\n}\n\n// email=git@noreply.alice.com\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"dns_proof\"\n}\n```\n\nJust an rough idea. I'd like to hear more opinions about this. :)", + "createdAt": "2026-05-16T17:41:23Z", + "references": [ + "at://did:plc:kmjtnzau4j5u3zyly2dm73jc/sh.tangled.repo.issue/3mafbqgjmg422", + "at://did:plc:pbjvqaziagcyv2vqodldn5op/sh.tangled.repo.issue/3mkudlykqus22", + "at://did:plc:3owqn5zl4lq3ckxo2jvhexvm/sh.tangled.repo.issue/3ltppi6k6zp22" + ], + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "discussion: `sh.tangled.email.resolveEmail`" + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlxuiurykt22", + "cid": "bafyreiba35lot4gsuxojp5httmlktx5kic7tivkkuyoevnlelymcteqvxy", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "For a knot to be active, it should:\n\n1. have matching owner (we don't need this once we deprecate the `*.member` lexicon [#335](https://tangled.org/tangled.org/core/issues/335))\n - expose owner DID in `/xrpc/sh.tangled.owner` endpoint\n - `at://{owner}/sh.tangled.knot/{knotHostname}` exists\n2. be registered to knotmirror\n3. expose valid `/events` websocket endpoint to stay active in knotmirror\n\nAll three(four) conditions should match. Ideally last two once we deprecate the `*.member` lexicon ([#335](https://tangled.org/tangled.org/core/issues/335)).\n\nWe currently show the owner verification state in appview but not knot activeness state. We should at least have an api to expose that information to public like atproto relay does.", + "createdAt": "2026-05-16T12:39:11Z", + "references": [ + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3maae23igov22" + ], + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "knotmirror: expose knot active state" + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3ml6n5ux4cq22", + "cid": "bafyreifgubyrelybvfnhkbqfh4xtqrzd35tqthmz7nieq3ejcj6ag664ty", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "## Context:\n[#320: proposal: trigger pipeline from spindle (not from knot)](https://tangled.org/tangled.org/core/issues/320)\n> currently, `sh.tangled.pipeline` event is emitted from tangled when git ref update triggered the pipeline.\n> Conceptually this is unnecessary logic for knot. We can make spindle to do this instead.\n> 1. spindle listen for `sh.tangled.git.refUpdate` from knot event stream\n> 2. when ref update happens, fetch repo from knot, parse `.tangled/workflows/*.yml` and trigger the pipeline.\n\n[#526: proposal: make the trigger event payload accessible to pipeline workflow runs](https://tangled.org/tangled.org/core/issues/526)\n> For comparison, GitHub Actions trigger events are unified with their webhooks, so the full webhook payload is available in triggered workflow ([example](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request)).\n>\n> Could Tangled do something similar? https://docs.tangled.org/webhooks#webhook-payload\n\nand maybe [#368: proposal: spindle rewrite (`sh.tangled.ci.*` and \"spindle adapters\")](https://tangled.org/tangled.org/core/issues/368)\n\n---\n\nSpindle is the core of automation in Tangled. Its entire existence is to run CI. And webhook is also heavily related to the automation, usually serving as a trigger for third-party CI services. If triggering pipeline event from spindle makes sense, considering pipeline & webhook trigger are pretty identical, I think it makes sense to just move entire webhook logic to the spindle.\n\nUsers will still be able to use current appview webhook, but internal implementation will be changed to spindle1.\n\nThis will also solve the centralization problem around tangled webhooks. If someone cannot trust Tangled, they can just use other spindle service or run their own.\n\n## Technical problems\n\nSpindle should listen to any events referencing the repository.\n- git event (refUpdate)\n- new issue/PR\n- new fork\n- new comment on issue PR\n- issue/PR state change\n\ngit event is ok, spindle is already listening to knotstream for that.\\\nOthers, basically atproto events are the problem. Ideally we want spindle to only subscribe for events related to its repos. To do that we can either:\n\n1. Just listen to entire firehose. Spindle requires quite high spec machine anyways.\n2. Introduce tangled-specific, jetstream-like service that understands the schema and stream records only related to the git repos.\n\nI personally think second option sounds more ideal. We will want that kind of service for various reasons.\n\n> edit (2026-05-15): now spindle embeds the tap, so it is actually consuming the entire firehose right now.\n\n## tl;dr\n\n- webhooks and pipelines are similar in both concept and implementation\n- implement webhook from spindle and not appview. current appview webhooks will just use spindle1 under the hood.\n- technical difficulties would be self-hosted spindles consuming repo-referencing records from the atmosphere.", + "createdAt": "2026-05-06T11:51:12Z", + "references": [ + "at://did:plc:5wdnwfs45bghuedlj3rdmani/sh.tangled.repo.issue/3mkxb5n7b2f22", + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mbtmkdhtik22", + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3m76435hnuu22" + ], + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "proposal: move webhook to spindle" + }, + "state": "open", + "commentCount": 0 + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3ml25o5fput22", + "cid": "bafyreifaksylt6g5jgqzwy7couvldip7kbeyoms4txvfiy7lfqqdfmdsnq", + "value": { + "$type": "sh.tangled.repo.issue", + "body": "## Why\n\nWe see bunch of new contributors these days but don't have an effective way to show them other than bsky release post.\n\n## How\n\nThis kind of indexing should be done from knotmirror, but knotmirror doesn't know about `email->did` connections... As a quick solution, I suggest to just return list of emails. We don't have that much contributors ***yet*** so ignoring pagination should be fine.", + "createdAt": "2026-05-04T20:03:17+03:00", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "title": "contributors tab" + }, + "state": "open", + "commentCount": 0 + } + ], + "cursor": "00065100e8273f400000f018" +} diff --git a/tests/fixtures/feed_comments_on_issue.json b/tests/fixtures/feed_comments_on_issue.json new file mode 100644 index 0000000..1a28d2e --- /dev/null +++ b/tests/fixtures/feed_comments_on_issue.json @@ -0,0 +1,58 @@ +{ + "items": [ + { + "uri": "at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.issue.comment/3mlqj32adah22", + "cid": "bafyreievcc2byatiuft6gbpp7av3vqhigep32xndxgwryjv55o2qguov6e", + "value": { + "$type": "sh.tangled.repo.issue.comment", + "body": { + "text": "both terms and privacy pages exist? like i said, deleting data will be introduced down the line. tangled is alpha and yes i do expect users to understand that. " + }, + "createdAt": "2026-05-13T17:25:56+03:00", + "replyTo": { + "cid": "bafkqaaa", + "uri": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue.comment/3mlqfzg424y22" + }, + "subject": { + "cid": "bafkqaaa", + "uri": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue/3mlnx7w4tt722" + } + } + }, + { + "uri": "at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.issue.comment/3mlq4z5txge22", + "cid": "bafyreih2tymla2t6rheflmru6h4b2x6gfk2ha32nonytge7jvyku7ljiqu", + "value": { + "$type": "sh.tangled.repo.issue.comment", + "body": { + "text": "we don't have such a thing on the appview at the moment, maybe it will be introduced down the line. for now you can head to pds.ls and delete your tangled records, it will reflect on all appviews automatically. " + }, + "createdAt": "2026-05-13T13:50:08+03:00", + "replyTo": { + "cid": "bafkqaaa", + "uri": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue.comment/3mlpvmt5e4o22" + }, + "subject": { + "cid": "bafkqaaa", + "uri": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue/3mlnx7w4tt722" + } + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.comment/3mlo2lpmgku22", + "cid": "bafyreicgwt46qxxno4a5vtxbp5mcz2v5ewau5pz24wwcjqxvma5sijirie", + "value": { + "$type": "sh.tangled.repo.issue.comment", + "body": { + "text": "Do you want to delete your tangled profile or your atproto account in general? Your `dominikg.dev` identity is hosted on `npmx.social` and includes your bluesky account." + }, + "createdAt": "2026-05-12T18:01:29+03:00", + "subject": { + "cid": "bafkqaaa", + "uri": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue/3mlnx7w4tt722" + } + } + } + ], + "cursor": null +} \ No newline at end of file diff --git a/tests/fixtures/issue_comment_records.json b/tests/fixtures/issue_comment_records.json new file mode 100644 index 0000000..0f15f0d --- /dev/null +++ b/tests/fixtures/issue_comment_records.json @@ -0,0 +1,35 @@ +{ + "records": [ + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.comment/3mlo2lpmgku22", + "cid": "bafyreicgwt46qxxno4a5vtxbp5mcz2v5ewau5pz24wwcjqxvma5sijirie", + "value": { + "body": "Do you want to delete your tangled profile or your atproto account in general? Your `dominikg.dev` identity is hosted on `npmx.social` and includes your bluesky account.", + "$type": "sh.tangled.repo.issue.comment", + "issue": "at://did:plc:ss2kvy4zxtbh72stxi22k26i/sh.tangled.repo.issue/3mlnx7w4tt722", + "createdAt": "2026-05-12T18:01:29+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.comment/3ml3tu2enry22", + "cid": "bafyreiho3dhbdhogtpwht4b4hz663e6ktwls7sj67betbri5izzbxdfd5y", + "value": { + "body": "oops wrong account", + "$type": "sh.tangled.repo.issue.comment", + "issue": "at://did:plc:njwa3qvknwqscrhdly466mgr/sh.tangled.repo.issue/3ml3tr7oxoq22", + "createdAt": "2026-05-05T12:12:58+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.comment/3mcluapi4mo22", + "cid": "bafyreifbxykfqtwsjjk32zaqslygmunp4mhd4tc7y5tq3g6bonjtyzgzaa", + "value": { + "body": "This might be a bug with `hx-boost` but cannot reproduce from Safari and Chromium. Which browser are you using?", + "$type": "sh.tangled.repo.issue.comment", + "issue": "at://did:plc:wh2g32kxs6xgduoibqdpne5j/sh.tangled.repo.issue/3mckxv5meuz22", + "createdAt": "2026-01-17T07:15:54+02:00" + } + } + ], + "cursor": "3mcluapi4mo22" +} \ No newline at end of file diff --git a/tests/fixtures/issue_new_record.json b/tests/fixtures/issue_new_record.json new file mode 100644 index 0000000..2e55e36 --- /dev/null +++ b/tests/fixtures/issue_new_record.json @@ -0,0 +1,13 @@ +{ + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mtsi7x4vmc5b", + "cid": "bafyreiavy33pmggndqjw5exaxjl3ji43xo5jfokhs76rmrb33gc5nzoclu", + "value": { + "body": "## Summary\n\nA request to `com.atproto.repo.uploadBlob` returned HTTP 413 when uploading a\n10,551,352-byte blob.\n\nThe blob was intended for a `sh.tangled.repo.artifact` record, whose Lexicon\npermits blobs up to 50 MiB (52,428,800 bytes).\n\n## Request\n\n- Date: 2026-08-24\n- Endpoint: `com.atproto.repo.uploadBlob`\n- Content-Type: `application/gzip`\n- Request body size: `10,551,352 bytes`\n- SHA-256:\n `a60752004df3502cd88b39c24e14c7eb4bff4eaa5926b1570cc42675685f3680`\n\nThe request body was within the limit declared by\n`sh.tangled.repo.artifact`.\n\n## Response\n\nThe request returned HTTP 413 with the following HTML body:\n\n```html\n\n413 Request Entity Too Large\n\n

413 Request Entity Too Large

\n
nginx
\n\n\n```\n\nThe response body identifies its server as nginx. This suggests that the\nrequest may have been rejected by an HTTP proxy before the AT Protocol service\nhandled it, but the responsible component and configured limit have not been\nidentified.\n\nBecause the blob upload failed, no `sh.tangled.repo.artifact` record was\ncreated.\n\nThe same file was also submitted through the Web UI. The UI displayed no error,\nbut no artifact record was created.\n\nAfter reducing the request body to `7,817,312 bytes`, the call to\n`com.atproto.repo.uploadBlob` succeeded and the artifact record could be\ncreated.\n\n## Expected behavior\n\nThe upload path should accept request bodies large enough to support the 50 MiB\nmaximum declared by `sh.tangled.repo.artifact`.\n\nIf a lower limit is intentional, the effective limit should be documented and\naligned with the artifact Lexicon.\n\nFor rejected requests:\n\n- The API should return a structured AT Protocol error rather than an HTML\n response.\n- The response should communicate the effective request-size limit.\n- The Web UI should display the upload failure.", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "com.atproto.repo.uploadBlob returns HTTP 413 for a 10.55 MB artifact allowed by the Lexicon", + "mentions": [], + "createdAt": "2026-08-24T04:37:39.517Z", + "references": [] + } +} diff --git a/tests/fixtures/issue_old_record.json b/tests/fixtures/issue_old_record.json new file mode 100644 index 0000000..92657b8 --- /dev/null +++ b/tests/fixtures/issue_old_record.json @@ -0,0 +1,13 @@ +{ + "uri": "at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.issue/3lxcnwjb7ff22", + "cid": "bafyreiauo4zraukm6orsw57ots6pgaqkh5styid2run2dvrkukclompmzy", + "value": { + "body": "abc", + "repo": "at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo/3lxcauknoye22", + "$type": "sh.tangled.repo.issue", + "owner": "did:plc:qfpnj4og54vl56wngdriaxug", + "title": "xyz", + "issueId": 2, + "createdAt": "" + } +} diff --git a/tests/fixtures/issue_states.json b/tests/fixtures/issue_states.json new file mode 100644 index 0000000..3d7ebdd --- /dev/null +++ b/tests/fixtures/issue_states.json @@ -0,0 +1,123 @@ +{ + "records": [ + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mtntoyg7iv22", + "cid": "bafyreibzo6ncafzffikrj3tofigumlsuwsxatrybijwsjjeq2lc3op6jau", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:hmoale4n4asiamsarmkp47or/sh.tangled.repo.issue/3mtmqd7dvrn22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-22T08:19:36.655Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mrkgncyiqi22", + "cid": "bafyreig2imfktpakkmammylqxsx3sj72vyincm3gct422did7nndcy23ne", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mn7pow5pwr22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T12:57:39.517Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mrkftykuti22", + "cid": "bafyreihoimgzgkeukj6xn54o537ar2ssza2zzuymfpilohvzdvxbpnm33a", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3m76435hnuu22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T12:43:29.724Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mrkfsnra7b22", + "cid": "bafyreih2grgya3hljhjc6vyqduzquom24jlb37sfkazorgdjqezdifue4m", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3maazutr2g322", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T12:42:44.843Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mrcsam5nms22", + "cid": "bafyreih7tewhpgvu4rsrdkpdyca2l4gvjyh24kgmarlbqsw2tvasoamv5m", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:3danwc67lo7obz2fmdg6jxcr/sh.tangled.repo.issue/3lkmjmogpli22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-23T12:03:59.91Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mqziwrqkg522", + "cid": "bafyreihudzw4uzpjqwgbod3vfwuecfhpvcdxcxact4juh4zocoxaj4psvi", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:6hbqm2oftpotwuw7gvvrui3i/sh.tangled.repo.issue/3mqzeqeevd422", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-19T19:23:28.906Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mqmraogekv22", + "cid": "bafyreiencdvs3k3iylf5vcswdgo2oie2wbgl7b56vk2m6adg2kmfw7olc4", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mqmjxilmrz22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-14T17:47:34.307Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mq26grdl5522", + "cid": "bafyreiaky7bvkwkfvdrszf5f7nbaa6lrcp6jxjdeeb65e4yoarjmibdouu", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3m7i45kp53z22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-07T08:23:02.302Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mq26fwgpmi22", + "cid": "bafyreidjifeb6wdvov7tpzc4varui4jpea3ukvj6geu4fvs5isouxwutra", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mbtmkdhtik22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-07T08:22:34.094Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3mpwbhypeen22", + "cid": "bafyreiblo6pbkrhbkcy77tdiu22fw4niugel6vrvwr67fbxx2mlj2rxlx4", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:kts3ofsxtgut4sv3pdfyh5kr/sh.tangled.repo.issue/3mpvrk53fgz22", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-05T19:06:45.855Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3lury52hj6322", + "cid": "bafyreidyq7thmdrh5cbvbc7fbfz6ztlumpo7p6xbwxnajesyqsj5lf7amq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "", + "state": "sh.tangled.repo.issue.state.closed" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue.state/3ludanmvlu622", + "cid": "bafyreidyq7thmdrh5cbvbc7fbfz6ztlumpo7p6xbwxnajesyqsj5lf7amq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "", + "state": "sh.tangled.repo.issue.state.closed" + } + } + ], + "cursor": "3ludanmvlu622" +} diff --git a/tests/fixtures/issue_states_reopened.json b/tests/fixtures/issue_states_reopened.json new file mode 100644 index 0000000..8fa0d0c --- /dev/null +++ b/tests/fixtures/issue_states_reopened.json @@ -0,0 +1,235 @@ +{ + "records": [ + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mtctsenkf2f3", + "cid": "bafyreida3umcaxqvws26ux5q3mczxds4plo3eawqyztft3w3j5qcelfecm", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msez3mgcesxo", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-17T23:22:13.020Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mt4p5ucklv22", + "cid": "bafyreif2l6mbbxtcavdl7xl2ug2pd4zzgi5rxovemrwmpur2heyq6asjyu", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mss2s66um2uf", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-15T12:43:11.396Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mt42c4ft4u22", + "cid": "bafyreiawkwf2ggzj65ce5lfi7i5mf6webm42i7jh5u7r4dpu4f3pizb47m", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msyqfboae2bv", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-15T06:29:45.531Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mss5phtytb22", + "cid": "bafyreibdncpfzbrrsod2r5gqr22qa4tlxaaf6hycicw4cwk2skm2n2egci", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msrwdw7abkfh", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-11T08:04:17.579Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3msrwik3gwsgv", + "cid": "bafyreidfnvcxnaqnlphc43mhqwh224gbm7d6hlscmg367lhija2m43thxm", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msrjkfmeo2pz", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-11T05:55:08.847Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3msfd4xelokcr", + "cid": "bafyreiehyvwqnnszjabq5homktkby75f763asqjdsgbgiz67qhpui7ric4", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msfd4c3gskmf", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-06T05:36:42.170Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3msfd4u3alkbi", + "cid": "bafyreidigdkbc5g27j4yjbb4zicboypqkbru26wtdbldmwwe33lfscomwm", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msfd4c3gskmf", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-08-06T05:36:38.719Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3msfd4r32hk7p", + "cid": "bafyreidsv6vkeuggkerzzatgrsa46bp7e4bv4aolb7e7iywmsjeg6ki4pq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3msfd4c3gskmf", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-08-06T05:36:35.567Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrsxvgkfgg22", + "cid": "bafyreianp72upc4btd3denldvirzdxhfl66e55wmfvtgy2467bczrx5jsi", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrhq5dwak2v5", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-29T22:27:43.202Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlogqtocsls", + "cid": "bafyreieglljg2vm2fiw224btaxfelnmjpuoow5hivshpdbu4th2vzkcj54", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrlog52jl2cb", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-27T00:49:48.831Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlognkmyczy", + "cid": "bafyreihl2fwb25bsufo3l53fy2gwvfhm2hblsg7yv74mc67oiabjhdyr6q", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrlog52jl2cb", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-07-27T00:49:45.390Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlogklj22sb", + "cid": "bafyreibhwm46nv2wreakylrmaxwl7plpcqnwklcnwe4gqiddn7xpcrkaha", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrlog52jl2cb", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-27T00:49:42.272Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlibohmt2vw", + "cid": "bafyreiaebsgdwebqrdgmkeuh4n25gyu5x7whea3ajasfdaqw7ijylyx4hq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrliahhx22zi", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T22:59:36.116Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlibjqhvsqy", + "cid": "bafyreibl5ebeijwqmwwue6bsrzt2tkzcxceilu5p255o73ov4at2yedwa4", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrliahhx22zi", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-07-26T22:59:31.163Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrlib3pips43", + "cid": "bafyreih2y5uat7kbcikrhffs4lowa4eghu7bhayq6vudhebypbx22hudtu", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrliahhx22zi", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T22:59:16.452Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrldnbyhd2a4", + "cid": "bafyreiddmhvdb2sbrjz3r7io2lupv5q2o2iewlbzzysp7vgrm5ozg7ijay", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrldly2qqst6", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T21:36:36.980Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrldn7ccicd3", + "cid": "bafyreidy5hvp5k7rbjul6f46p6ecompsmls75737tswukisknvdu7cueoy", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrldly2qqst6", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-07-26T21:36:34.157Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrldn4iv5si6", + "cid": "bafyreic5py5bynrl3eh52icfzfrjt2cvmxoaqbuiyex4nkshw37y4i3nsq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrldly2qqst6", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T21:36:31.227Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrj5cgafe2uj", + "cid": "bafyreic7doipeehb57xh2izxkqba4y4msv6jkjbfvxocs5yglkycp7suhe", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrj5bamvn27h", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-07-26T00:37:50.409Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrj5cdkzvsld", + "cid": "bafyreibk2h5bqmxuwqew7f5w2jmrpf2cv5onkcwadyd5wyxvd7rzukuzga", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrj5bamvn27h", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-26T00:37:47.611Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mrix4csten22", + "cid": "bafyreih7ujt2jfgyqpynziypan7jivz6jdaq43hvfnryba7q4fuevizzrq", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mrhrvqehjcsa", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-25T22:47:03.04Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mreuvta3rkmd", + "cid": "bafyreiapapedh3quhwqdr3hbpmbqib5dxsz2rje3sckcsglbxt3l2c34tm", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mresuy3jics4", + "state": "sh.tangled.repo.issue.state.open", + "createdAt": "2026-07-24T07:56:58.934Z" + } + }, + { + "uri": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue.state/3mreuudo2p2oa", + "cid": "bafyreierzgbr4yl3mt2dr63qpicgwt6faktxedtpiyccxljx6gjwrbmmbe", + "value": { + "$type": "sh.tangled.repo.issue.state", + "issue": "at://did:plc:jge3zxi7lgrfnvhzcgrimeo7/sh.tangled.repo.issue/3mresuy3jics4", + "state": "sh.tangled.repo.issue.state.closed", + "createdAt": "2026-07-24T07:56:09.061Z" + } + } + ], + "cursor": "3mreuudo2p2oa" +} diff --git a/tests/fixtures/pds_issues_page.json b/tests/fixtures/pds_issues_page.json new file mode 100644 index 0000000..69de61d --- /dev/null +++ b/tests/fixtures/pds_issues_page.json @@ -0,0 +1,132 @@ +{ + "records": [ + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlzxvsoidf22", + "cid": "bafyreig5yxy7br5lcu6eodyehchlwrfcxaevcc2x77gp7256hsvcuo5oma", + "value": { + "body": "For example in PR compose page, label checkboxes should not be interactive when form is submitted. [`hx-disabled-elt`](https://htmx.org/attributes/hx-disabled-elt/) can be used for this.", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "disable entire form while submitting", + "createdAt": "2026-05-17T11:45:24+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlygnbdl4622", + "cid": "bafyreihsdn6d73jwbrf43hiljklhuyzbaxjqfc6m5abmdylyntepabpmkm", + "value": { + "body": "km should wait until ongoing resync to complete.\n\nreproduce steps:\n1. create super large repo \n2. km fails to sync it due to timeout\n3. see tree/index pages are rendered as empty\n4. see blob page does render ", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "bug: don't respond when repository is not fully synced yet.", + "createdAt": "2026-05-16T18:03:45Z" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlyffalt7s22", + "cid": "bafyreig56sv6segyauw6vze2okyeac5momdlrudolegn35h4k3tkvyz4ty", + "value": { + "body": "context: [discord thread](https://discord.com/channels/1361963801993285692/1361991850327674932/1505199771445493891)\n\n> what do y’all think of like, atproto-identity <-> email mapping being public?\\\n> ...\\\n> problem is we should verify that in both way. boltless should not claim that he is `lewis@example.com`\\\n> ...\\\n> it would be nice if stateless client can inherit from specific appviews.\n\nAlso related:\n- [#133 fix: allow multiple subdomains in author email addresses](https://tangled.org/tangled.org/core/issues/133)\n- [#342 Can't associate @users.noreply.github.com email to account, fails `net.LookupMX(domain)`](https://tangled.org/tangled.org/core/issues/342)\n- [#515 Allow private GitHub email address associations](https://tangled.org/tangled.org/core/issues/515)\n\nBasically any services with `sh.tangled.email.resolveEmail` and `sh.tangled.email.resolveDid` will be email verification service (usually an appview). This service is expected to perform email verification against the user in various methods.\n\nExample responses of `sh.tangled.email.resolveEmail`:\n```jsonc\n// email=alice@gmail.com\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"email_challenge\"\n}\n\n// email=alice.com@users.noreply.tangled.org\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"service_assigned\"\n}\n\n// email=git@noreply.alice.com\n{\n \"did\": \"did:plc:alice\",\n \"reason\": \"dns_proof\"\n}\n```\n\nJust an rough idea. I'd like to hear more opinions about this. :)", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "discussion: `sh.tangled.email.resolveEmail`", + "createdAt": "2026-05-16T17:41:23Z", + "references": [ + "at://did:plc:kmjtnzau4j5u3zyly2dm73jc/sh.tangled.repo.issue/3mafbqgjmg422", + "at://did:plc:pbjvqaziagcyv2vqodldn5op/sh.tangled.repo.issue/3mkudlykqus22", + "at://did:plc:3owqn5zl4lq3ckxo2jvhexvm/sh.tangled.repo.issue/3ltppi6k6zp22" + ] + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mlxuiurykt22", + "cid": "bafyreiba35lot4gsuxojp5httmlktx5kic7tivkkuyoevnlelymcteqvxy", + "value": { + "body": "For a knot to be active, it should:\n\n1. have matching owner (we don't need this once we deprecate the `*.member` lexicon [#335](https://tangled.org/tangled.org/core/issues/335))\n - expose owner DID in `/xrpc/sh.tangled.owner` endpoint\n - `at://{owner}/sh.tangled.knot/{knotHostname}` exists\n2. be registered to knotmirror\n3. expose valid `/events` websocket endpoint to stay active in knotmirror\n\nAll three(four) conditions should match. Ideally last two once we deprecate the `*.member` lexicon ([#335](https://tangled.org/tangled.org/core/issues/335)).\n\nWe currently show the owner verification state in appview but not knot activeness state. We should at least have an api to expose that information to public like atproto relay does.", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "knotmirror: expose knot active state", + "createdAt": "2026-05-16T12:39:11Z", + "references": [ + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3maae23igov22" + ] + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3ml6n5ux4cq22", + "cid": "bafyreifgubyrelybvfnhkbqfh4xtqrzd35tqthmz7nieq3ejcj6ag664ty", + "value": { + "body": "## Context:\n[#320: proposal: trigger pipeline from spindle (not from knot)](https://tangled.org/tangled.org/core/issues/320)\n> currently, `sh.tangled.pipeline` event is emitted from tangled when git ref update triggered the pipeline.\n> Conceptually this is unnecessary logic for knot. We can make spindle to do this instead.\n> 1. spindle listen for `sh.tangled.git.refUpdate` from knot event stream\n> 2. when ref update happens, fetch repo from knot, parse `.tangled/workflows/*.yml` and trigger the pipeline.\n\n[#526: proposal: make the trigger event payload accessible to pipeline workflow runs](https://tangled.org/tangled.org/core/issues/526)\n> For comparison, GitHub Actions trigger events are unified with their webhooks, so the full webhook payload is available in triggered workflow ([example](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request)).\n>\n> Could Tangled do something similar? https://docs.tangled.org/webhooks#webhook-payload\n\nand maybe [#368: proposal: spindle rewrite (`sh.tangled.ci.*` and \"spindle adapters\")](https://tangled.org/tangled.org/core/issues/368)\n\n---\n\nSpindle is the core of automation in Tangled. Its entire existence is to run CI. And webhook is also heavily related to the automation, usually serving as a trigger for third-party CI services. If triggering pipeline event from spindle makes sense, considering pipeline & webhook trigger are pretty identical, I think it makes sense to just move entire webhook logic to the spindle.\n\nUsers will still be able to use current appview webhook, but internal implementation will be changed to spindle1.\n\nThis will also solve the centralization problem around tangled webhooks. If someone cannot trust Tangled, they can just use other spindle service or run their own.\n\n## Technical problems\n\nSpindle should listen to any events referencing the repository.\n- git event (refUpdate)\n- new issue/PR\n- new fork\n- new comment on issue PR\n- issue/PR state change\n\ngit event is ok, spindle is already listening to knotstream for that.\\\nOthers, basically atproto events are the problem. Ideally we want spindle to only subscribe for events related to its repos. To do that we can either:\n\n1. Just listen to entire firehose. Spindle requires quite high spec machine anyways.\n2. Introduce tangled-specific, jetstream-like service that understands the schema and stream records only related to the git repos.\n\nI personally think second option sounds more ideal. We will want that kind of service for various reasons.\n\n> edit (2026-05-15): now spindle embeds the tap, so it is actually consuming the entire firehose right now.\n\n## tl;dr\n\n- webhooks and pipelines are similar in both concept and implementation\n- implement webhook from spindle and not appview. current appview webhooks will just use spindle1 under the hood.\n- technical difficulties would be self-hosted spindles consuming repo-referencing records from the atmosphere.", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "proposal: move webhook to spindle", + "createdAt": "2026-05-06T11:51:12Z", + "references": [ + "at://did:plc:5wdnwfs45bghuedlj3rdmani/sh.tangled.repo.issue/3mkxb5n7b2f22", + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mbtmkdhtik22", + "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3m76435hnuu22" + ] + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3ml25o5fput22", + "cid": "bafyreifaksylt6g5jgqzwy7couvldip7kbeyoms4txvfiy7lfqqdfmdsnq", + "value": { + "body": "## Why\n\nWe see bunch of new contributors these days but don't have an effective way to show them other than bsky release post.\n\n## How\n\nThis kind of indexing should be done from knotmirror, but knotmirror doesn't know about `email->did` connections... As a quick solution, I suggest to just return list of emails. We don't have that much contributors ***yet*** so ignoring pagination should be fine.", + "repo": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", + "$type": "sh.tangled.repo.issue", + "title": "contributors tab", + "repoDid": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "createdAt": "2026-05-04T20:03:17+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mkwxjf7opo22", + "cid": "bafyreifkum2hivpfx5w72bfrkb7rxeaayzg3xz2aszqvv6zb2txlla7gqi", + "value": { + "body": "`![foo](bar)` should be rendered as:\n\n```html\n\"foo\"\n```", + "repo": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", + "$type": "sh.tangled.repo.issue", + "title": "render markdown image as links", + "repoDid": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "createdAt": "2026-05-03T13:35:16+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mktu7hbiig22", + "cid": "bafyreiermj674phn4vdp5kcklecg2awvjuent4mlpgw3mhypw2rhg235ye", + "value": { + "body": "`repo.compare` is memory intensive and basically just a read operation.\n\nWe should fix its memory intensiveness ofc, but ideally this should not be a job of self-hosted knots. AppView should use KnotMirror for any read operations. So two things to do:\n\n- fix `repo.compare` memory usage\n- prefer knotmirror for `repo.compare`", + "repo": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", + "$type": "sh.tangled.repo.issue", + "title": "move `repo.compare` to knotmirror", + "repoDid": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "createdAt": "2026-05-02T07:58:02+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mkttralzib22", + "cid": "bafyreih76ut4n4sifnqgsbewnlcatbkfvrxrtiq7hei3vk3mnafjzy4cja", + "value": { + "body": "We need dedicated event stream for knots to sync git repos & detect git-push events.\n\nKnotMirror already has a relay-like service baked in, listening to `requestCrawl` calls and managing websocket connections with all public knots.\n\nSo we can just extract that part & add outbound event stream. This will make knotstream consumers like AppView or spindle way simpler as they just need to listen to single stream.\n\nListening to entire stream can be inefficient for services like spindle which only needs events from specific knots so we might also need jetstream-like service too, but current knotstream isn't that massive like atproto firehose so it will be fine in foreseeable future.", + "repo": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", + "$type": "sh.tangled.repo.issue", + "title": "knot-relay", + "repoDid": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "createdAt": "2026-05-02T07:50:06+03:00" + } + }, + { + "uri": "at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.issue/3mkookre2hi22", + "cid": "bafyreidipuazfmsnihhulmmijwdktgkyia2z63k5rxdl2kde56pzexss4e", + "value": { + "body": "Would be cool if users can catch good-first-issues from timeline.", + "repo": "did:plc:j5hmlfdrwkvtxm7cjmu7j2is", + "$type": "sh.tangled.repo.issue", + "title": "show gfi in timeline", + "createdAt": "2026-04-30T06:33:41+03:00" + } + } + ], + "cursor": "3mkookre2hi22" +}