spec/edit_cgi_spec.cr @ trunk
spec
scale = 0.75 fontscale = 1.500 box wid 1.380 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1 box wid 1.380 ht 0.340 with .nw at (0.000, 0.000) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.022) fill 0xffffff color 0xffffff line from (0.140, 0.000) to (0.140, -0.340) color white line from (1.240, 0.000) to (1.240, -0.340) color white text "require 'spec'" color white at (0.690, -0.170)→ /pikchrshow
crystal source
require "spec"
json block
{
"kind": "require",
"name": "spec",
"doc": null,
"children": []
}
compiler/crystal/syntax
scale = 0.75 fontscale = 1.500 box wid 2.796 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.796 ht 0.340 with .nw at (0.000, -0.420) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.443) fill 0xffffff color 0xffffff line from (0.140, -0.420) to (0.140, -0.760) color white line from (2.656, -0.420) to (2.656, -0.760) color white text "require 'compiler/crystal/syntax'" color white at (1.398, -0.590)→ /pikchrshow
crystal source
require "compiler/crystal/syntax"
json block
{
"kind": "require",
"name": "compiler/crystal/syntax",
"doc": null,
"children": []
}
../src/gpcr/apply
scale = 0.75 fontscale = 1.500 box wid 2.255 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.255 ht 0.340 with .nw at (0.000, -0.840) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.863) fill 0xffffff color 0xffffff line from (0.140, -0.840) to (0.140, -1.180) color white line from (2.115, -0.840) to (2.115, -1.180) color white text "require '../src/gpcr/apply'" color white at (1.127, -1.010)→ /pikchrshow
crystal source
require "../src/gpcr/apply"
json block
{
"kind": "require",
"name": "../src/gpcr/apply",
"doc": null,
"children": []
}
../src/gpcr/speak
scale = 0.75 fontscale = 1.500 box wid 2.304 ht 0.340 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.304 ht 0.340 with .nw at (0.000, -1.260) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.283) fill 0xffffff color 0xffffff line from (0.140, -1.260) to (0.140, -1.600) color white line from (2.164, -1.260) to (2.164, -1.600) color white text "require '../src/gpcr/speak'" color white at (1.152, -1.430)→ /pikchrshow
crystal source
require "../src/gpcr/speak"
json block
{
"kind": "require",
"name": "../src/gpcr/speak",
"doc": null,
"children": []
}
../src/gpcr/render
scale = 0.75 fontscale = 1.500 box wid 2.343 ht 0.340 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.343 ht 0.340 with .nw at (0.000, -1.680) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.703) fill 0xffffff color 0xffffff line from (0.140, -1.680) to (0.140, -2.020) color white line from (2.203, -1.680) to (2.203, -2.020) color white text "require '../src/gpcr/render'" color white at (1.172, -1.850)→ /pikchrshow
crystal source
require "../src/gpcr/render"
json block
{
"kind": "require",
"name": "../src/gpcr/render",
"doc": null,
"children": []
}
../src/gpcr/edit
scale = 0.75 fontscale = 1.500 box wid 2.117 ht 0.340 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.117 ht 0.340 with .nw at (0.000, -2.100) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.123) fill 0xffffff color 0xffffff line from (0.140, -2.100) to (0.140, -2.440) color white line from (1.977, -2.100) to (1.977, -2.440) color white text "require '../src/gpcr/edit'" color white at (1.059, -2.270)→ /pikchrshow
crystal source
require "../src/gpcr/edit"
json block
{
"kind": "require",
"name": "../src/gpcr/edit",
"doc": null,
"children": []
}
private def fences_balanced?(md : String) : Bool
open = false
marker = ""
md.each_line do |line|
s = line.rstrip
unless open
if m = s.match(/^(`{3,})/)
marker = m[1]
open = true
end
else
if s == marker
open = false
marker = ""
end
end
end
!open
end
# GPCR edit CGI spec.
#
# Tests the four key requirements from ticket a3d00815e7 at the module level:
# (1) "wrap block 8 with repeat 3" round-trips: new source parses, renderer
# shows the repeat C-shape, fences are balanced.
# (2) A def-target phrase ("wrap block 1 with repeat 3") produces the
# kind-aware rejection hint, not a silent failure.
# (3) "undo" after a wrap op restores the previous source.
# (4) Fences are balanced after the wrap op is applied.
#
# Uses spec/fixtures/edit_fixture.cr (the standard edit corpus):
# Block 1 = on_start (:def — Primitive)
# Block 2 = set_user_led (:def — Primitive)
# Block 3 = wait (:def — Primitive)
# Block 4 = forever (:def — Primitive)
# Block 5 = blink (:def — Graph)
# Block 6 = on_start (call inside blink)
# Block 7 = forever (c-shape call inside blink)
# Block 8 = set_user_led true (first call inside forever body)
# ...
EDIT_FIXTURE_PATH = File.join(__DIR__, "fixtures", "edit_fixture.cr")
describe "GPCR::Edit — voice-style edit CGI specs" do
describe "(1) 'wrap block 8 with repeat 3' round-trips" do
it "Speak.parse produces a WrapOp for the phrase" do
result = GPCR::Speak.parse("wrap block 8 with repeat 3")
result.should be_a(GPCR::Ops::WrapOp)
op = result.as(GPCR::Ops::WrapOp)
t = op.target.as(GPCR::Ops::IndexTarget)
t.index.should eq(8)
op.block.should eq("repeat")
op.slots["times"]?.should eq("3")
end
it "applying the WrapOp produces source with '3.times do'" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(op)
buf.source.should contain("3.times do")
end
it "new source parses as valid Crystal" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(op)
Crystal::Parser.new(buf.source).parse
true.should be_true
end
it "rendered Markdown shows the repeat C-shape" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(op)
md = GPCR::Renderer.new(buf.source).render
md.should contain("3.times")
end
end
describe "(2) def-target phrase shows kind-aware rejection hint" do
it "wrapping block 1 (a :def kind) raises OpError with 'definition' in the hint" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 1 with repeat 3").as(GPCR::Ops::WrapOp)
raised = false
msg = ""
begin
buf.apply_one(op)
rescue e : GPCR::Ops::OpError
raised = true
msg = e.message.to_s
end
raised.should be_true
msg.should contain("definition")
msg.should contain("wrap")
end
it "the hint mentions the block index" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 1 with repeat 3").as(GPCR::Ops::WrapOp)
raised = false
msg = ""
begin
buf.apply_one(op)
rescue e : GPCR::Ops::OpError
raised = true
msg = e.message.to_s
end
raised.should be_true
msg.should contain("block 1")
end
end
describe "(3) 'undo' restores the previous source" do
it "after wrap then undo, source no longer contains '3.times do'" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
wrap_op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(wrap_op)
buf.source.should contain("3.times do")
undo_op = GPCR::Ops::UndoOp.new(1)
buf.apply_one(undo_op)
buf.source.should_not contain("3.times do")
end
it "after undo the source still parses as valid Crystal" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
wrap_op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(wrap_op)
undo_op = GPCR::Ops::UndoOp.new(1)
buf.apply_one(undo_op)
Crystal::Parser.new(buf.source).parse
true.should be_true
end
it "Speak.parse('undo') returns an UndoOp with count 1" do
result = GPCR::Speak.parse("undo")
result.should be_a(GPCR::Ops::UndoOp)
result.as(GPCR::Ops::UndoOp).count.should eq(1)
end
end
describe "(4) fences balanced after wrap op" do
it "Markdown fences are balanced in the pre-wrap render" do
source = File.read(EDIT_FIXTURE_PATH)
md = GPCR::Renderer.new(source).render
fences_balanced?(md).should be_true
end
it "Markdown fences are balanced after 'wrap block 8 with repeat 3'" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(op)
md = GPCR::Renderer.new(buf.source).render
fences_balanced?(md).should be_true
end
it "Markdown fences are balanced after undo" do
source = File.read(EDIT_FIXTURE_PATH)
buf = GPCR::Ops::Buffer.new(source)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::WrapOp)
buf.apply_one(op)
buf.apply_one(GPCR::Ops::UndoOp.new(1))
md = GPCR::Renderer.new(buf.source).render
fences_balanced?(md).should be_true
end
end
describe "(WIP) edit_wip_page_markdown" do
it "contains the WIP notice" do
page = edit_wip_page_markdown("/ext/gpcr", "examples/blink.cr")
page.should contain("Work In Progress")
page.should contain("Edit-rendering is not yet implemented")
end
it "links back to the editor with file_path when provided" do
page = edit_wip_page_markdown("/ext/gpcr", "examples/blink.cr")
page.should contain("/ext/gpcr/edit?file=examples/blink.cr")
end
it "links back to bare editor when no file_path" do
page = edit_wip_page_markdown("/ext/gpcr", "")
page.should contain("/ext/gpcr/edit")
page.should_not contain("?file=")
end
end
describe "(5) seed-path: first GET on empty state seeds from examples/blink.cr" do
it "edit_current_source returns examples/blink.cr content when UV has no current file" do
tmp = File.tempname("gpcr_test_seed", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
expected = GPCR::Edit::SEED_SOURCE
result = edit_current_source(tmp)
result.should eq(expected)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
end
describe "(6) form structure for Enter-to-submit and action URL" do
it "form action ends with /edit when no file_path" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should match(/<form[^>]+action="[^"]*\/edit"/)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "form contains exactly one submit button" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form2", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
buttons = page.scan(/<button[^>]+type="submit"/)
buttons.size.should eq(1)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "phrase input appears inside the form" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form3", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
form_start = page.index("<form")
form_end = page.index("</form>")
input_pos = page.index(%(<input type="text" name="phrase"))
form_start.should_not be_nil
form_end.should_not be_nil
input_pos.should_not be_nil
input_pos.not_nil!.should be > form_start.not_nil!
input_pos.not_nil!.should be < form_end.not_nil!
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "edit_page_markdown returns page content, not a 302 header" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form4", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should_not contain("Status: 302")
page.should contain("# GPCR Voice-Style Edit")
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "apply success path renders updated source inline (no redirect)" do
source = File.read(EDIT_FIXTURE_PATH)
op = GPCR::Speak.parse("wrap block 8 with repeat 3").as(GPCR::Ops::Op)
buf = GPCR::Ops::Buffer.new(source)
buf.apply_one(op)
new_source = buf.source
tmp = File.tempname("gpcr_test_apply_inline", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, new_source, "", "/ext/gpcr")
page.should contain("# GPCR Voice-Style Edit")
page.should contain("3.times do")
page.should_not contain("Status: 302")
page.should_not contain("edit-wip")
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "form uses GET method" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form_method", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should match(/<form[^>]+method="get"/)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "form includes hidden file input when file_path given" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form_file", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr", "examples/blink.cr")
page.should match(/<input[^>]+type="hidden"[^>]+name="file"/)
page.should contain(%[value="examples/blink.cr"])
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "form does not include hidden file input when no file_path" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_form_nofile", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should_not match(/<input[^>]+type="hidden"[^>]+name="file"/)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
end
# Ticket 6912f4a1e9: no-refresh submit via fetch-swap. The plain-form path
# (no JS) is exactly the form/route tested in section (6) above, unchanged.
# These specs cover the progressive-enhancement addition: the swap-target
# markup the JS asset hooks into, and the asset itself.
describe "(7) fetch-swap progressive enhancement" do
it "wraps the rendering in a gpcr-edit-render div, opened and closed as a standalone HTML block" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_swap_render", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should contain(%(<div id="gpcr-edit-render">\n\n))
render_start = page.index(%(<div id="gpcr-edit-render">)).not_nil!
render_end = page.index("</div>", render_start).not_nil!
form_start = page.index("<form").not_nil!
render_end.should be < form_start
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "wraps Last Result and Edit History in a gpcr-edit-result div, after the form" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_swap_result", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should contain(%(<div id="gpcr-edit-result">\n\n))
form_end = page.index("</form>").not_nil!
result_start = page.index(%(<div id="gpcr-edit-result">)).not_nil!
result_end = page.index("</div>", result_start).not_nil!
result_start.should be > form_end
last_result_pos = page.index("## Last Result").not_nil!
history_pos = page.index("## Edit History").not_nil!
last_result_pos.should be > result_start
history_pos.should be < result_end
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "form carries a stable id for the JS asset to hook the submit event" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_swap_formid", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should match(/<form id="gpcr-edit-form"[^>]+method="get"/)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "references the external edit.js asset via src=, never inline script" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_swap_script", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
page = edit_page_markdown(tmp, source, "", "/ext/gpcr")
page.should contain(%(<script src="/ext/gpcr/edit.js" defer></script>))
page.should_not match(/<script(?![^>]*\bsrc=)[^>]*>[^<]/)
page.should_not match(/\bon[a-z]+="/)
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "CSP header (script-src 'self') is still sent, satisfied by the same-origin script" do
source = File.read(EDIT_FIXTURE_PATH)
tmp = File.tempname("gpcr_test_swap_csp", ".fossil")
begin
Process.run("fossil", ["init", tmp], env: {"USER" => "gpcr-spec"}, error: Process::Redirect::Close)
buf = IO::Memory.new
env = {
"PATH" => "/usr/bin:/bin",
"GATEWAY_INTERFACE" => "CGI/1.1",
"REQUEST_METHOD" => "GET",
"PATH_INFO" => "edit",
"QUERY_STRING" => "",
"SCRIPT_NAME" => "/ext/gpcr",
"FOSSIL_REPOSITORY" => tmp,
"FOSSIL_CAPABILITIES" => "ohi",
}
bin = File.join(Dir.current, "bin", "gpcr")
if File.exists?(bin)
Process.run(bin, env: env, clear_env: true, output: buf, error: Process::Redirect::Close)
body = buf.to_s
body.should contain("Content-Security-Policy: script-src 'self'")
body.should contain(%(<script src="/ext/gpcr/edit.js" defer></script>))
end
ensure
File.delete(tmp) if File.exists?(tmp)
end
end
it "GPCR::Edit::CLIENT_JS is non-empty and targets the swap-region ids" do
GPCR::Edit::CLIENT_JS.should contain("gpcr-edit-render")
GPCR::Edit::CLIENT_JS.should contain("gpcr-edit-result")
GPCR::Edit::CLIENT_JS.should contain("gpcr-edit-form")
GPCR::Edit::CLIENT_JS.should_not contain("<script")
end
it "GET /ext/gpcr/edit.js serves the client JS with an application/javascript Content-Type" do
bin = File.join(Dir.current, "bin", "gpcr")
if File.exists?(bin)
buf = IO::Memory.new
env = {
"PATH" => "/usr/bin:/bin",
"GATEWAY_INTERFACE" => "CGI/1.1",
"REQUEST_METHOD" => "GET",
"PATH_INFO" => "edit.js",
"QUERY_STRING" => "",
"SCRIPT_NAME" => "/ext/gpcr",
"FOSSIL_REPOSITORY" => "/nonexistent/gpcr_editjs_test.fossil",
"FOSSIL_CAPABILITIES" => "oh",
}
Process.run(bin, env: env, clear_env: true, output: buf, error: Process::Redirect::Close)
body = buf.to_s
body.should start_with("Content-Type: application/javascript\r\n\r\n")
body.should contain("gpcr-edit-render")
body.should_not contain("Status: 403")
end
end
end
end