From 5f683cba31c9c06073f7d4bc984afafbbfc4527f Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 23:16:10 +0100 Subject: [PATCH] fix: journal:fix-headings runs from the application CLI The command opens the database with lookupDB, as journal:image-variants does: without a published database it opens database.dsn, and without a DSN it names SUMMER_DATABASE__DSN. --- README.md | 2 +- console/fix_headings.go | 14 +++++++----- image_variants_cli_test.go | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b67570c..bc5c8f9 100644 --- a/README.md +++ b/README.md @@ -529,7 +529,7 @@ The export and import commands write JSON files (one per post slug), matching th | `journal:export-posts` | `--path` (default `journal-export`), `--dry-run` | Writes one JSON file per post, including non-default-locale translations, with a progress bar over the posts. Attachments are omitted. Ends with `Exported N posts to: `. | | `journal:import-posts` | `--path` (default `journal-export`), `--force`, `--dry-run` | Creates posts from those files, one line per post; while the H1 setting is on, H1 headings are stored as H2 and the count is printed. Existing slugs are skipped unless `--force` updates them. Ends with `Results: N created, N updated, N skipped, N failed`. | | `journal:image-variants` | `--all` | Writes the thumbnails and WebP copies of featured images at `golem15.journal.image_widths` that are missing (with `--all`, checks every featured image) and prints `Checked N featured images, wrote N variants, skipped N.`, or `Every featured image has its variants.` | -| `journal:fix-headings` | `--apply`, `--dry-run` (the default) | Converts H1 to H2 in stored post content (every locale) and category pillar text while the H1 setting is on, firing a record event per changed post and category. Prints `Would convert N H1 headings in N posts and N categories. Run with --apply to save.`, `Converted N H1 headings in N posts and N categories.` or `No H1 headings found in post or category content.`; refuses with exit code 1 while the setting is off. | +| `journal:fix-headings` | `--apply`, `--dry-run` (the default) | Converts H1 to H2 in stored post content (every locale) and category pillar text while the H1 setting is on, firing a record event per changed post and category. Prints `Would convert N H1 headings in N posts and N categories. Run with --apply to save.`, `Converted N H1 headings in N posts and N categories.` or `No H1 headings found in post or category content.`; refuses with exit code 1 while the setting is off. Runs from the application CLI like `journal:image-variants`: without a published database it opens `database.dsn` (`SUMMER_DATABASE__DSN`). | | `journal:reindex` | `--fresh` | Syncs every post into the search index in batches of 200: published posts are upserted, the others removed. `--fresh` first drops the collection and recreates it from the schema. Refuses, sending nothing, when the engine is not configured or `search_use_typesense` is off. Ends with `Reindexed: N synced, N removed, N failed` and exits non-zero on any failure. | ```sh diff --git a/console/fix_headings.go b/console/fix_headings.go index e159878..502f079 100644 --- a/console/fix_headings.go +++ b/console/fix_headings.go @@ -2,7 +2,6 @@ package console import ( "context" - "errors" "fmt" "git.golem15.com/golem15/sm-journal-plugin/classes" @@ -13,8 +12,10 @@ import ( // FixHeadings is journal:fix-headings: it converts H1 to H2 in stored // Journal rich text (post content in every locale and category pillar text) // while the Journal H1 setting is on. It is a dry run unless --apply is -// given; with the setting off it refuses with exit code 1. Its output (UI-SPEC -// C27 with the P33-54 wording, English only): +// given; with the setting off it refuses with exit code 1. It runs from the +// generated application CLI: without a published database it opens +// database.dsn (lookupDB) and, without one, names SUMMER_DATABASE__DSN. Its +// output (UI-SPEC C27 with the P33-54 wording, English only): // // - dry run: "Would convert :count H1 headings in :posts posts and :categories categories. Run with --apply to save." // - applied: "Converted :count H1 headings in :posts posts and :categories categories." @@ -47,10 +48,11 @@ journal_fix_headings runs the same conversion.`, {Command: "journal:fix-headings --apply", Description: "Convert them and save"}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { - db := mustDB(app) - if db == nil { - return errors.New("journal: database is not configured") + db, done, err := lookupDB(ctx, app) + if err != nil { + return err } + defer done() if !classes.EffectiveH1ToH2(ctx, db, appConfig(app)) { out.Error(classes.H1ConversionOffText) return bonfire.Reported(classes.ErrH1ConversionOff) diff --git a/image_variants_cli_test.go b/image_variants_cli_test.go index 527d28b..bc7bedd 100644 --- a/image_variants_cli_test.go +++ b/image_variants_cli_test.go @@ -97,3 +97,50 @@ func TestImageVariantsCommandCLI(t *testing.T) { t.Fatalf("the CLI's WebP %s is not in the bucket: %v", variants[1].WebPKey, err) } } + +// TestFixHeadingsCommandCLI pins L1 WR-01 (D-50): journal:fix-headings in +// the generated CLI, where no database is published: without a DSN it +// names the setting, with one it opens database.dsn and prints the dry run. +func TestFixHeadingsCommandCLI(t *testing.T) { + if testShort() { + t.Skip("Postgres") + } + env := newPostsEnv(t) + var name string + if err := env.gdb.Raw(`SELECT current_database()`).Scan(&name).Error; err != nil { + t.Fatal(err) + } + u, err := url.Parse(adminDSN) + if err != nil { + t.Fatal(err) + } + u.Path = "/" + name + post := models.Post{Title: "Cli heading", Slug: "cli-heading", Content: "

A

x

", ContentHTML: "

A

x

"} + if err := env.gdb.Create(&post).Error; err != nil { + t.Fatal(err) + } + run := func(environ ...string) (int, string) { + t.Helper() + cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: append([]string{"SUMMER_ENV=development"}, environ...)}) + if err != nil { + t.Fatal(err) + } + app := backpack.New(cfg) + var buf strings.Builder + root, err := bonfire.NewRootIO("summer", []bonfire.Command{console.FixHeadings(app)}, strings.NewReader(""), &buf, &buf) + if err != nil { + t.Fatal(err) + } + return bonfire.Execute(context.Background(), root, []string{"journal:fix-headings"}), buf.String() + } + if code, out := run(); code == 0 || !strings.Contains(out, "database is not configured (set SUMMER_DATABASE__DSN)") { + t.Fatalf("without a DSN: %d\n%s", code, out) + } + code, out := run("SUMMER_DATABASE__DSN="+u.String(), "SUMMER_APP__KEY="+base64.StdEncoding.EncodeToString(bytes.Repeat([]byte("k"), 32))) + if code != 0 || !strings.Contains(out, "Would convert 1 H1 headings in 1 posts and 0 categories. Run with --apply to save.") { + t.Fatalf("journal:fix-headings from the CLI: %d\n%s", code, out) + } + if p := storedPost(t, env, post.ID); p.Content != "

A

x

" { + t.Fatalf("the dry run wrote %q", p.Content) + } +}