Login
gpcr/spec/edit_session_spec.cr
Login

[edit]

spec/edit_session_spec.cr @ trunk

spec

require '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)

crystal source

require "spec"

json block

{
  "kind": "require",
  "name": "spec",
  "doc": null,
  "children": []
}

file_utils

require 'file_utils'
scale = 0.75
fontscale = 1.500
box wid 1.645 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.645 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 (1.505, -0.420) to (1.505, -0.760) color white
text "require 'file_utils'" color white at (0.823, -0.590)

crystal source

require "file_utils"

json block

{
  "kind": "require",
  "name": "file_utils",
  "doc": null,
  "children": []
}

../src/gpcr/edit

require '../src/gpcr/edit'
scale = 0.75
fontscale = 1.500
box wid 2.117 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.117 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 (1.977, -0.840) to (1.977, -1.180) color white
text "require '../src/gpcr/edit'" color white at (1.059, -1.010)

crystal source

require "../src/gpcr/edit"

json block

{
  "kind": "require",
  "name": "../src/gpcr/edit",
  "doc": null,
  "children": []
}

private FOSSIL_ENV = {"USER" => "gpcr-spec"}

# Creates a throwaway repo with one committed file, returns {repo, rev}.
# rev is the checkout hash of the seeding commit (the file's base_revision).
private def make_repo_with_file(file_path : String, content : String) : {repo: String, rev: String, checkout_dir: String}
  repo = File.tempname("gpcr_session_test", ".fossil")
  checkout_dir = File.tempname("gpcr_session_test_co")
  Dir.mkdir(checkout_dir)
  Process.run("fossil", ["init", repo], env: FOSSIL_ENV, error: Process::Redirect::Close)
  Process.run("fossil", ["open", repo], chdir: checkout_dir, env: FOSSIL_ENV, error: Process::Redirect::Close)
  File.write(File.join(checkout_dir, file_path), content)
  Process.run("fossil", ["add", file_path], chdir: checkout_dir, env: FOSSIL_ENV, error: Process::Redirect::Close)
  Process.run("fossil", ["commit", "-m", "seed", "--no-warnings"], chdir: checkout_dir, env: FOSSIL_ENV, error: Process::Redirect::Close)
  info_out = IO::Memory.new
  Process.run("fossil", ["info"], chdir: checkout_dir, env: FOSSIL_ENV, output: info_out, error: Process::Redirect::Close)
  rev = info_out.to_s.lines.find(&.starts_with?("checkout:")).try { |l| l.split(/\s+/)[1]? } || ""
  {repo: repo, rev: rev, checkout_dir: checkout_dir}
end

# Commits a new revision of file_path in an already-open checkout, returns
# the new checkout hash. Used to simulate the file changing out from under
# a bound session (the stale-base-revision scenario).
private def commit_new_revision(checkout_dir : String, file_path : String, content : String) : String
  File.write(File.join(checkout_dir, file_path), content)
  Process.run("fossil", ["commit", "-m", "change", "--no-warnings"], chdir: checkout_dir, env: FOSSIL_ENV, error: Process::Redirect::Close)
  info_out = IO::Memory.new
  Process.run("fossil", ["info"], chdir: checkout_dir, env: FOSSIL_ENV, output: info_out, error: Process::Redirect::Close)
  info_out.to_s.lines.find(&.starts_with?("checkout:")).try { |l| l.split(/\s+/)[1]? } || ""
end

private def cleanup_repo(paths : {repo: String, rev: String, checkout_dir: String})
  File.delete(paths[:repo]) if File.exists?(paths[:repo])
  FileUtils.rm_rf(paths[:checkout_dir])
end

FIXTURE_SRC = File.read(File.join(__DIR__, "fixtures", "edit_fixture.cr"))

describe "GPCR::Edit editing session lifecycle" do
  describe "(1) bind" do
    it "creates a session with generation 1, the given file/revision, not closed" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        session.file_path.should eq("blink.cr")
        session.base_revision.should eq(paths[:rev])
        session.generation.should eq(1)
        session.closed.should be_false
        session.session_id.should_not be_empty
      ensure
        cleanup_repo(paths)
      end
    end

    it "persists the session — a fresh load by id returns the same bind" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        reloaded = session_load(paths[:repo], session.session_id)
        reloaded.should_not be_nil
        reloaded.not_nil!.file_path.should eq("blink.cr")
        reloaded.not_nil!.generation.should eq(1)
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(2) one successful phrase-to-JSON edit" do
    it "wrap block 8 with repeat 3 applies through a bound session" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Ok)
        result.source.should_not be_nil
        result.source.not_nil!.should contain("3.times do")
        result.op_json.should_not be_nil
        result.op_json.not_nil!.should_not be_empty
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(3) invalid operation" do
    it "wrapping a def target (block 1) is rejected, not silently applied" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        result = session_edit(paths[:repo], session.session_id, "wrap block 1 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Invalid)
        result.message.should contain("definition")
        result.source.should be_nil
      ensure
        cleanup_repo(paths)
      end
    end

    it "an unparseable phrase is rejected with the parser's hint" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        result = session_edit(paths[:repo], session.session_id, "asdf not a real phrase", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Invalid)
        result.message.should_not be_empty
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(4) stale base revision" do
    it "rejects an edit once the file has changed since bind" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        commit_new_revision(paths[:checkout_dir], "blink.cr", FIXTURE_SRC + "\n# changed underneath the session\n")
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Stale)
        result.message.should contain("stale")
      ensure
        cleanup_repo(paths)
      end
    end

    it "a session bound AFTER the change is not stale" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        new_rev = commit_new_revision(paths[:checkout_dir], "blink.cr", FIXTURE_SRC + "\n# changed before bind\n")
        session = session_bind(paths[:repo], "blink.cr", new_rev)
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Ok)
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(5) capability denial" do
    it "rejects an edit when caps lack check-in (i)" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "oh")
        result.status.should eq(GPCR::Edit::SessionStatus::Denied)
      ensure
        cleanup_repo(paths)
      end
    end

    it "the 's' superuser capability is still sufficient without literal 'i'" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "s")
        result.status.should eq(GPCR::Edit::SessionStatus::Ok)
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(6) close" do
    it "closes an open session" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        session_close(paths[:repo], session.session_id).should be_true
        reloaded = session_load(paths[:repo], session.session_id)
        reloaded.not_nil!.closed.should be_true
      ensure
        cleanup_repo(paths)
      end
    end

    it "rejects further edits through a closed session" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        session = session_bind(paths[:repo], "blink.cr", paths[:rev])
        session_close(paths[:repo], session.session_id)
        result = session_edit(paths[:repo], session.session_id, "wrap block 8 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Closed)
      ensure
        cleanup_repo(paths)
      end
    end
  end

  describe "(7) rebind" do
    it "creates a new session with generation + 1 for the same file" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        first = session_bind(paths[:repo], "blink.cr", paths[:rev])
        session_close(paths[:repo], first.session_id)
        second = session_rebind(paths[:repo], first.session_id, paths[:rev])
        second.should_not be_nil
        second.not_nil!.file_path.should eq("blink.cr")
        second.not_nil!.generation.should eq(2)
        second.not_nil!.closed.should be_false
        second.not_nil!.session_id.should_not eq(first.session_id)
      ensure
        cleanup_repo(paths)
      end
    end

    it "the rebound session accepts edits even though the prior one is closed" do
      paths = make_repo_with_file("blink.cr", FIXTURE_SRC)
      begin
        first = session_bind(paths[:repo], "blink.cr", paths[:rev])
        session_close(paths[:repo], first.session_id)
        second = session_rebind(paths[:repo], first.session_id, paths[:rev]).not_nil!
        result = session_edit(paths[:repo], second.session_id, "wrap block 8 with repeat 3", "ohi")
        result.status.should eq(GPCR::Edit::SessionStatus::Ok)
      ensure
        cleanup_repo(paths)
      end
    end
  end
end