// promote_test.go proves RFC 0011 §1's `chat.promote` end to end on a
// REAL repository: a run of channel messages becomes a forum thread in
// the DAG, the channel is told where its conversation went, and the
// reference somebody typed in chat — which the window would have
// forgotten in seven days — is a durable backlink on the artifact's own
// page afterwards (§14).
package harness
import (
"encoding/json"
"net/http"
"net/url"
"strings"
"testing"
)
// promoted folds [from,to] through the op and answers the thread root.
func (f *refForge) promote(from, to uint64, title string) (string, string) {
f.t.Helper()
form := url.Values{"from": {itoa(from)}, "to": {itoa(to)}}
if title != "" {
form.Set("title", title)
}
code, body := f.do("POST", "/api/v1/r/alpha/chat/promote", "dan", form)
if code != http.StatusOK {
f.t.Fatalf("chat.promote: status %d (%s)", code, body)
}
var out struct {
Hash string `json:"hash"`
Title string `json:"title"`
Count int `json:"count"`
Seq uint64 `json:"seq"`
}
if err := json.Unmarshal([]byte(body), &out); err != nil {
f.t.Fatal(err)
}
if out.Hash == "" {
f.t.Fatalf("promote opened no thread: %s", body)
}
return out.Hash, body
}
func (f *refForge) chat(text string) uint64 {
f.t.Helper()
code, body := f.do("POST", "/api/v1/r/alpha/chat", "dan", url.Values{"text": {text}})
if code != http.StatusOK {
f.t.Fatalf("chat post: status %d (%s)", code, body)
}
var posted struct {
Seq uint64 `json:"seq"`
}
if err := json.Unmarshal([]byte(body), &posted); err != nil {
f.t.Fatal(err)
}
return posted.Seq
}
// The whole promotion: the thread carries the folded messages and a
// link back to the channel, the channel carries a robot line pointing
// at the thread, and the ticket somebody named in chat now has a
// durable "Referenced by" row that outlives the chat window.
func TestPromoteFoldsChatIntoADurableThread(t *testing.T) {
f := newRefForge(t)
id := f.newTicket("the bug being discussed")
prefix := id[:8]
first := f.chat("the lease is per store, not per node")
f.chat("agreed — see [" + prefix + "] for the failing case")
last := f.chat("let us write it down")
root, _ := f.promote(first, last, "")
// The thread exists and reads as the conversation it folded.
_, page := f.do("GET", "/r/alpha/forum/"+root, "dan", nil)
if page == "" {
t.Fatal("the promoted thread has no page")
}
for _, want := range []string{
"the lease is per store, not per node",
"agreed",
"let us write it down",
} {
if !strings.Contains(page, want) {
t.Fatalf("the thread is missing the message %q:\n%s", want, page)
}
}
// The opener links back to the channel it came from, and to the
// ends of the run: a reader who wants the live version can get
// there without guessing sequences.
if !strings.Contains(page, `href="/r/alpha/chat"`) {
t.Fatalf("the promoted thread has no back-link to the channel:\n%s", page)
}
if !strings.Contains(page, `href="/r/alpha/chat/`+itoa(first)+`"`) {
t.Fatalf("the promoted thread does not link the run's first message:\n%s", page)
}
// The channel is told, once, as a robot line.
_, history := f.do("GET", "/r/alpha/chat", "dan", nil)
if !strings.Contains(history, `data-kind="robot"`) {
t.Fatalf("the channel was never told about the promotion:\n%s", history)
}
// The line names the run and the thread by REFERENCE, so the
// resolver has already turned both into links by the time a reader
// sees them — which is the point of writing it as tokens.
if !strings.Contains(history, "promoted") ||
!strings.Contains(history, `/r/alpha/forum/`+root[:12]) {
t.Fatalf("the robot line does not point at the promoted thread:\n%s", history)
}
if !strings.Contains(history, `data-hook="robot-mark"`) {
t.Fatalf("the robot line is not drawn as one:\n%s", history)
}
// And the point of the whole exercise: the reference typed in chat
// is now a DURABLE backlink derived from the thread's own text,
// beside the window-bounded chat row rather than instead of it.
_, ticket := f.do("GET", "/r/alpha/ticket/"+id, "dan", nil)
if !strings.Contains(ticket, `id="tkt-refs"`) {
t.Fatalf("no durable Referenced-by section on the ticket page:\n%s", ticket)
}
if !strings.Contains(ticket, `/r/alpha/forum/`+root[:12]) {
t.Fatalf("the durable backlink does not point at the promoted thread:\n%s", ticket)
}
}
// A title given explicitly is the thread's; an omitted one is the
// first line of the first message promoted.
func TestPromoteTitles(t *testing.T) {
f := newRefForge(t)
seq := f.chat("why the window is seven days")
_, body := f.promote(seq, seq, "")
if !strings.Contains(body, `"title":"why the window is seven days"`) {
t.Fatalf("default title did not come from the first message: %s", body)
}
seq2 := f.chat("another thing entirely")
_, body2 := f.promote(seq2, seq2, "Window retention")
if !strings.Contains(body2, `"title":"Window retention"`) {
t.Fatalf("explicit title was not used: %s", body2)
}
}
// A range the window no longer holds — or one that never held
// anything — is refused in words, not archived as an empty thread.
func TestPromoteRefusesAnUnpromotableRange(t *testing.T) {
f := newRefForge(t)
seq := f.chat("the only message")
code, body := f.do("POST", "/api/v1/r/alpha/chat/promote", "dan",
url.Values{"from": {itoa(seq + 50)}, "to": {itoa(seq + 60)}})
if code != http.StatusBadRequest {
t.Fatalf("promoting an empty range: status %d (%s)", code, body)
}
code, body = f.do("POST", "/api/v1/r/alpha/chat/promote", "dan",
url.Values{"from": {itoa(seq)}, "to": {itoa(seq - 1)}})
if code != http.StatusBadRequest {
t.Fatalf("promoting a reversed range: status %d (%s)", code, body)
}
}