FT-WP-0001-T06: add the temporary quoted-remembrance composer.
Assistant: grok Assistant-Session: 01a1097f-0521-7fd1-8343-23631f633e2e
This commit is contained in:
parent
43fd92f375
commit
94ee3f4f9c
7 changed files with 1438 additions and 4 deletions
135
cmd/compose/main.go
Normal file
135
cmd/compose/main.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Command compose is the temporary quoted-remembrance draft tool.
|
||||
//
|
||||
// It writes a review queue the caller names and, once a person has approved
|
||||
// a draft with an explicit consent basis, POSTs that draft to an adapter's
|
||||
// /v1/channel-posts. It has no path into the adapter package or Telegram.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tegwick/fluid-telegram/internal/compose"
|
||||
)
|
||||
|
||||
const usage = `compose — temporary quoted-remembrance drafts
|
||||
|
||||
compose draft --entry <file> --queue <dir> (--source-url <url> | --base-url <url>)
|
||||
compose approve --queue <dir> --post <id> --reviewer <name> --consent explicit|standing|not-required
|
||||
compose publish --queue <dir> --post <id> --adapter <base>
|
||||
|
||||
Consent is never defaulted. A draft is not published until approve has
|
||||
recorded a reviewer and a consent basis the caller supplied.
|
||||
`
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "compose:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New(usage)
|
||||
}
|
||||
switch args[0] {
|
||||
case "draft":
|
||||
return cmdDraft(args[1:])
|
||||
case "approve":
|
||||
return cmdApprove(args[1:])
|
||||
case "publish":
|
||||
return cmdPublish(args[1:])
|
||||
case "-h", "--help", "help":
|
||||
fmt.Fprint(os.Stderr, usage)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q\n%s", args[0], usage)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdDraft(args []string) error {
|
||||
fs := flag.NewFlagSet("draft", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
entry := fs.String("entry", "", "hall entry markdown")
|
||||
queue := fs.String("queue", "", "review queue directory")
|
||||
sourceURL := fs.String("source-url", "", "explicit source entry URL")
|
||||
baseURL := fs.String("base-url", "", "base URL; /hall-of-helix/<id> is appended")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *entry == "" || *queue == "" {
|
||||
return errors.New("draft requires --entry and --queue")
|
||||
}
|
||||
raw, err := os.ReadFile(*entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := compose.Compose(string(raw), compose.Source{URL: *sourceURL, Base: *baseURL})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q, err := compose.Open(*queue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.Put(d); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(d.PostID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdApprove(args []string) error {
|
||||
fs := flag.NewFlagSet("approve", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
queue := fs.String("queue", "", "review queue directory")
|
||||
post := fs.String("post", "", "draft post_id")
|
||||
reviewer := fs.String("reviewer", "", "person approving the draft")
|
||||
consent := fs.String("consent", "", "explicit, standing, or not-required")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *queue == "" || *post == "" || *reviewer == "" {
|
||||
return errors.New("approve requires --queue, --post, and --reviewer")
|
||||
}
|
||||
if *consent == "" {
|
||||
return errors.New("--consent is required (explicit, standing, or not-required); it is not defaulted")
|
||||
}
|
||||
q, err := compose.Open(*queue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := q.Approve(*post, *reviewer, *consent); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(*post)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdPublish(args []string) error {
|
||||
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
queue := fs.String("queue", "", "review queue directory")
|
||||
post := fs.String("post", "", "draft post_id")
|
||||
adapter := fs.String("adapter", "", "adapter base URL")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *queue == "" || *post == "" || *adapter == "" {
|
||||
return errors.New("publish requires --queue, --post, and --adapter")
|
||||
}
|
||||
q, err := compose.Open(*queue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := q.Publish(context.Background(), *adapter, *post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("published %s %d\n", d.PostID, d.Publication.StatusCode)
|
||||
return nil
|
||||
}
|
||||
117
cmd/compose/main_test.go
Normal file
117
cmd/compose/main_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const cliEntry = `---
|
||||
id: hall-worker-cli
|
||||
type: worker-entry
|
||||
display_name: CLI
|
||||
status: draft
|
||||
---
|
||||
|
||||
# A title
|
||||
|
||||
## What I would want remembered
|
||||
|
||||
Remember this line.
|
||||
`
|
||||
|
||||
func TestCLIDraftApprovePublish(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
entry := filepath.Join(dir, "entry.md")
|
||||
if err := os.WriteFile(entry, []byte(cliEntry), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queue := filepath.Join(dir, "queue")
|
||||
if err := run([]string{"draft", "--entry", entry, "--queue", queue, "--base-url", "https://example.test"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := onlyPostID(t, queue)
|
||||
raw, err := os.ReadFile(filepath.Join(queue, id+".json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "reviewed_by") || strings.Contains(string(raw), "subject_consent") {
|
||||
t.Fatalf("draft file has review fields: %s", raw)
|
||||
}
|
||||
err = run([]string{"approve", "--queue", queue, "--post", id, "--reviewer", "Ada"})
|
||||
if err == nil {
|
||||
t.Fatal("approve without --consent succeeded")
|
||||
}
|
||||
|
||||
var body map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Errorf("decode: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = io.WriteString(w, `{"state":"published"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if _, err := os.Stat(filepath.Join(queue, id+".json")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run([]string{"publish", "--queue", queue, "--post", id, "--adapter", srv.URL}); err == nil {
|
||||
t.Fatal("publish before approval succeeded")
|
||||
}
|
||||
if body != nil {
|
||||
t.Fatalf("publish before approval sent a body: %#v", body)
|
||||
}
|
||||
if err := run([]string{"approve", "--queue", queue, "--post", id, "--reviewer", "Ada", "--consent", "explicit"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run([]string{"publish", "--queue", queue, "--post", id, "--adapter", srv.URL}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["post_id"] != id || body["reviewed_by"] != "Ada" || body["subject_consent"] != "explicit" || body["variant"] != "quoted-remembrance-v1" {
|
||||
t.Fatalf("POST body = %#v", body)
|
||||
}
|
||||
if body["source_entry_url"] != "https://example.test/hall-of-helix/hall-worker-cli" {
|
||||
t.Fatalf("source url = %#v", body["source_entry_url"])
|
||||
}
|
||||
if !strings.Contains(body["body"].(string), "Remember this line.") {
|
||||
t.Fatalf("body = %#v", body["body"])
|
||||
}
|
||||
}
|
||||
|
||||
func onlyPostID(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
ents, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var id string
|
||||
for _, e := range ents {
|
||||
if !strings.HasSuffix(e.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var d struct {
|
||||
PostID string `json:"post_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != "" {
|
||||
t.Fatal("queue has more than one draft")
|
||||
}
|
||||
id = d.PostID
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("queue is empty")
|
||||
}
|
||||
return id
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue