# frozen_string_literal: true RSpec.describe DiscourseAi::Personas::ArtifactUpdateStrategies::Diff do fab!(:user) fab!(:post) fab!(:artifact) { Fabricate(:ai_artifact) } fab!(:llm_model) let(:llm) { llm_model.to_llm } let(:instructions) { "Update the button color to red" } let(:strategy) do described_class.new( llm: llm, post: post, user: user, artifact: artifact, artifact_version: nil, instructions: instructions, ) end describe "#apply" do it "processes simple search/replace blocks" do original_css = ".button { color: blue; }" artifact.update!(css: original_css) response = <<~RESPONSE [CSS] <<<<<<< SEARCH .button { color: blue; } ======= .button { color: red; } >>>>>>> REPLACE [/CSS] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expect(artifact.versions.last.css).to eq(".button { color: red; }") end it "handles multiple search/replace blocks in the same section" do original_css = <<~CSS .button { color: blue; } .text { font-size: 12px; } CSS artifact.update!(css: original_css) response = <<~RESPONSE [CSS] <<<<<<< SEARCH .button { color: blue; } ======= .button { color: red; } >>>>>>> REPLACE <<<<<<< SEARCH .text { font-size: 12px; } ======= .text { font-size: 16px; } >>>>>>> REPLACE [/CSS] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expected = <<~CSS.strip .button { color: red; } .text { font-size: 16px; } CSS expect(artifact.versions.last.css.strip).to eq(expected.strip) end it "handles non-contiguous search/replace using ..." do original_css = <<~CSS body { color: red; } .button { color: blue; } .alert { background-color: green; } CSS artifact.update!(css: original_css) response = <<~RESPONSE [CSS] <<<<<<< SEARCH body { ... background-color: green; } ======= body { color: red; } >>>>>>> REPLACE [/CSS] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expect(artifact.versions.last.css).to eq("body {\n color: red;\n}") end it "can handle removal with blank blocks" do original_css = <<~CSS body { color: red; } .button { color: blue; } CSS artifact.update!(css: original_css) response = <<~RESPONSE [CSS] <<<<<<< SEARCH body { color: red; } ======= >>>>>>> REPLACE [/CSS] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expect(artifact.versions.last.css.strip).to eq(".button {\n color: blue;\n}") end it "tracks failed searches" do original_css = ".button { color: blue; }" artifact.update!(css: original_css) response = <<~RESPONSE [CSS] <<<<<<< SEARCH .button { color: green; } ======= .button { color: red; } >>>>>>> REPLACE [/CSS] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expect(strategy.failed_searches).to contain_exactly( { section: :css, search: ".button { color: green; }" }, ) expect(artifact.versions.last.css).to eq(original_css) end it "handles complete section replacements" do original_html = "
old content
" artifact.update!(html: original_html) response = <<~RESPONSE [HTML]
new content
[/HTML] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } expect(artifact.versions.last.html.strip).to eq("
new content
") end it "ignores empty or 'no changes' sections part 1" do original = { html: "
content
", css: ".button { color: blue; }", js: "console.log('test');", } artifact.update!(html: original[:html], css: original[:css], js: original[:js]) response = <<~RESPONSE [HTML] no changes [/HTML] [CSS] (NO CHANGES) [/CSS] [JavaScript] <<<<<<< SEARCH console.log('test'); ======= console.log('(no changes)'); >>>>>>> REPLACE [/JavaScript] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } version = artifact.versions.last expect(version.html).to eq(original[:html]) expect(version.css).to eq(original[:css]) expect(version.js).to eq("console.log('(no changes)');") end it "ignores empty or 'no changes' section part 2" do original = { html: "
content
", css: ".button { color: blue; }", js: "console.log('test');", } artifact.update!(html: original[:html], css: original[:css], js: original[:js]) response = <<~RESPONSE [HTML] (no changes) [/HTML] [CSS] [/CSS] [JavaScript] <<<<<<< SEARCH console.log('test'); ======= console.log('updated'); >>>>>>> REPLACE [/JavaScript] RESPONSE DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply } version = artifact.versions.last expect(version.html).to eq(original[:html]) expect(version.css).to eq(original[:css]) expect(version.js).to eq("console.log('updated');") end end end