• v0.50.0 b23f34d03f

    joeyr released this 2026-08-26 10:56:10 -05:00 | 65 commits to main since this release

    list_discussion_entries returned "(unknown)" as the author of every post
    in a live thread, and it looked like an answer.
    Nineteen entries in a graded
    introductions discussion, a clean 200, no error, no flag — and a reader with
    no way to tell "Canvas was asked and had no name" from "the lookup never
    happened". The second one was true. The connector then reported the user's
    required replies as 0 of 2 done when 1 was, stated as fact, because the
    only attribution left was the prose: their initial post ended -Daniel and was
    found, their reply did not and was not.

    The cause is one field on one payload. A /view node names its author with
    a bare user_id; user_name and the nested user object belong to
    /entries, which this client stopped calling in 0.49.0 for being twice the
    price. The name in a /view response lives in the participants roster and
    nowhere else. The client read user_name, got nothing every time, and had a
    default sitting there to make it presentable.

    Two habits let it through, and both are worth naming.

    A probe measured one payload and the design generalised to the other. C6
    asked "what does an entry carry for identity" against /entries rows —
    correctly, that is where user_name is — and the same session decided /view
    on cost. Nothing re-asked the identity question of the payload that won.
    A measurement is about the request that produced it. When a probe changes
    which endpoint ships, every finding taken from the other one is unverified
    again.

    The fixture agreed with the code because the same person wrote both. This
    project's oldest failure mode, and tests/test_canvas.py even documented the
    agreement as a feature: _entry carried user_name and the nested user
    "on purpose", as the fields the tool must drop. They are not on a /view node
    at all. 1399 tests were green against a payload Canvas does not send.

    And the default made it silent. "(unknown)" is a plausible-looking string,
    so a wrong answer built on it carries no seam. A fallback that cannot be
    distinguished from a result is worse than no fallback.

    Fixed

    • author is joined out of the participants roster, which is where a
      /view payload keeps names. user_name is still preferred when a payload
      carries one, so nothing regresses if this client ever reads /entries again.

      The privacy decision is unchanged. The roster is read, reduced to
      {id: display_name}, and dropped — it covers people who never posted and
      carries avatars and profile links, and none of that leaves the client. The
      entry's own user_id does not leave either: a Canvas user id addresses a
      profile on a public course host.

    • An unresolved author is author: null with author_unresolved: true,
      never a string. The thread also reports unresolved_authors with a note
      saying the authorship is UNKNOWN rather than anonymous — do not attribute
      those posts, and do not count them as anybody's reply.

    Added

    • is_self on every entry, from the account's own Canvas id rather than
      from the text. "Have I already replied" is the question a graded discussion
      forces, and text-matching answers it wrongly the moment somebody does not
      sign a post.

      It is absent, not false, when the account could not be identified. A
      false there answers the question wrongly instead of declining it, which is
      this release's own defect one layer down.

    • own_entry_count, own_reply_count and people_replied_to on the
      thread. own_reply_count counts only posts answering somebody else, and
      people_replied_to counts distinct people — because "reply to two
      classmates" is not satisfied by two replies to one of them, nor by continuing
      your own sub-thread, and a withdrawn post is not a completed one.

      Counted across the whole tree rather than the returned slice, which is
      the reason the client derives them instead of leaving the caller to tally
      is_self rows. Past more_entries the user's own reply can sit at 101; a
      tally of the returned rows would say zero and read as a fact.

      people_replied_to says people, not classmates, on purpose — /view
      names participants without their role, so the instructor is indistinguishable
      from a student here.

    • own_participation_unknown with a note when the account could not be
      identified: none of those counts are present, and the note forbids the
      fallback that caused all of this — reading the prose for a name.

    • _self_user_id — one /api/v1/users/self, cached for the process. It
      runs after the thread is read and swallows every failure, including an
      expired session: ordered the other way, a bad answer from it would take down
      a tool that reads threads perfectly well without it. check_session was the
      obvious home and was declined — boot's request exists to prove the account
      can see coursework, and failing a boot over a field only one tool reads
      trades a working tenant for a convenience.

    Changed

    • tests/test_canvas.py's /view fixture now matches the payload Canvas
      sends
      — nodes carry user_id and no name, and _view builds the roster
      the names have to be joined out of. The old fixture is the reason this was
      invisible to the suite, so the docstring on _entry says not to put a name
      back on a node.

    • The thread log line carries unresolved_authors, identity and own.
      The first is the number that would have named this defect from the log alone
      — 19 of 19, for a release — on a line nobody had a reason to read. Counts
      only: no id and no name, as before.

    Not verified live

    The fix is proved against the suite and not against Canvas. The session
    cookie this tenant runs on lives in a Swarm secret; there is no working local
    credential, so the shape of a /view payload here rests on the failure report
    (19 of 19 unresolved, which only the missing field explains) and on
    /api/v1/users/self being an unmeasured request. Read the boot log and one
    thread through the deployed connector before calling this closed
    — this
    project's standing rule that a live pass finds what a green suite cannot has
    now been paid for twice on this tool.

    Downloads