char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
80904d5
main
1import { assert , assertEquals , assertRejects , assertStringIncludes } from "@std/assert" ; 2import { chmod , readFile , symlink , unlink , writeFile } from "node:fs/promises" ; 3import { join } from "node:path" ; 4import { localEnvironment } from "@berry/api/environment.ts" ; 5import { testExtension } from "@berry/api/testing.ts" ; 6import diff from "./mod.ts" ; 7import type { Reply , Shown } from "./types.ts" ; 8import type { OpenFile , Save , Saved } from "./edit-types.ts" ; 9import { onPath , tempDir } from "@berry/util" ; 10 11const IDENTITY = { 12JJ_USER :"Test" , 13JJ_EMAIL :"test@example.com" , 14GIT_AUTHOR_NAME :"Test" , 15GIT_AUTHOR_EMAIL :"test@example.com" , 16GIT_COMMITTER_NAME :"Test" , 17GIT_COMMITTER_EMAIL :"test@example.com" , 18GIT_CONFIG_GLOBAL :"/dev/null" , 19JJ_CONFIG :"/dev/null" , 20}; 21 22/** For setting repositories up; the extension reads them with whatever configuration it finds. */ 23async function run ( cwd :string , program :string , ...args :string []) { 24const result = await new Deno . Command ( program , { 25 args, 26 cwd, 27env :IDENTITY , 28stdout :"piped" , 29stderr :"piped" , 30}). output (); 31if ( ! result . success ) throw new Error ( new TextDecoder (). decode ( result . stderr )); 32return new TextDecoder (). decode ( result . stdout ). trim (); 33} 34 35async function repository ( test :( root :string ) => Promise < void >) { 36await usingrootDir = await tempDir ({ prefix :"diff-test-" }); 37const root = rootDir . path ; 38await test ( root ); 39} 40 41/** What a view that had nothing would be shown. */ 42const shown = async ( root :string , route = "" ) :Promise < Shown > => { 43const reply = await testExtension ( diff , { env :localEnvironment ( root ) }) 44. request ( "show" , { route}) as Reply ; 45return { ...reply , files :reply . files . map (({ diff}) => diff ! ) }; 46}; 47 48/** A save from the session "review" of `content` over what `opened` read. */ 49const saving = ( opened :OpenFile , content :string , path = opened . file . path ) :Save => ({ 50session :"review" , 51id :"edit" , 52working :opened . working , 53 path, 54version :opened . version , 55 content, 56}); 57 58const summary = ({ files} :Shown ) => 59files . map (({ path, status, hunks}) => ({ path, status, lines :hunks . flatMap (( h ) => h . lines ) })); 60 61Deno . test ({ 62name : 63"jj: the working copy's changes come whole, its history is browsable, and edits show at once" , 64ignore :! onPath ( "jj" ), 65fn :() => 66repository ( async ( root ) => { 67await run ( root , "jj" , "git" , "init" ); 68await writeFile ( join ( root , "a.txt" ), "one\ntwo\nthree\n" ); 69await run ( root , "jj" , "commit" , "-m" , "first\n\nwith a body" ); 70await writeFile ( join ( root , "a.txt" ), "one\n2\nthree\n" ); 71 72const working = await shown ( root ); 73assertEquals ( working . vcs , "jj" ); 74assertEquals ( working . root , root ); 75assertEquals ( working . revision . workingCopy , true ); 76assertEquals ( 77working . revision . change ! . full , 78await run ( 79root , 80"jj" , 81"log" , 82"--ignore-working-copy" , 83"--no-graph" , 84"-r" , 85"@" , 86"-T" , 87"change_id" , 88), 89); 90assertEquals ( 91working . revision . commit ! . full , 92await run ( 93root , 94"jj" , 95"log" , 96"--ignore-working-copy" , 97"--no-graph" , 98"-r" , 99"@" , 100"-T" , 101"commit_id" , 102), 103); 104assertEquals ( summary ( working ), [ 105{ path :"a.txt" , status :"modified" , lines :[ " one" , "-two" , "+2" , " three" ] }, 106]); 107// the root commit, which never has changes, is left out 108assertEquals ( working . log . map (( r ) => [ r . route === "" , r . description ]), [ 109[ true , "" ], 110[ false , "first\n\nwith a body\n" ], 111]); 112 113const first = await shown ( root , working . log [ 1 ]. route ); 114assertEquals ( first . revision . description , "first\n\nwith a body\n" ); 115assertEquals ( summary ( first ), [ 116{ path :"a.txt" , status :"added" , lines :[ "+one" , "+two" , "+three" ] }, 117]); 118 119await writeFile ( join ( root , "b.txt" ), "new\n" ); 120assertEquals (( await shown ( root )). files . map (({ path}) => path ), [ "a.txt" , "b.txt" ]); 121}), 122}); 123 124Deno . test ({ 125name :"git: the working tree includes untracked files, and each commit shows against its parent" , 126ignore :! onPath ( "git" ), 127fn :() => 128repository ( async ( root ) => { 129await run ( root , "git" , "init" , "--quiet" ); 130const before = await shown ( root ); 131assertEquals ( before . vcs , "git" ); 132assertEquals ( before . log . length , 1 ); 133await writeFile ( join ( root , "a.txt" ), "one\ntwo\n" ); 134await writeFile ( join ( root , ".gitignore" ), "ignored\n" ); 135await run ( root , "git" , "add" , "-A" ); 136await run ( root , "git" , "commit" , "--quiet" , "-m" , "first" ); 137await run ( root , "git" , "config" , "core.abbrev" , "12" ); 138await writeFile ( join ( root , "a.txt" ), "one\n2\n" ); 139await writeFile ( join ( root , "new file.txt" ), "hello\n" ); 140await writeFile ( join ( root , "ignored" ), "secret\n" ); 141 142const working = await shown ( root ); 143assertEquals ( summary ( working ), [ 144{ path :"a.txt" , status :"modified" , lines :[ " one" , "-two" , "+2" ] }, 145{ path :"new file.txt" , status :"added" , lines :[ "+hello" ] }, 146]); 147assertEquals ( working . log . map (( r ) => [ r . route === "" , r . description . trim ()]), [ 148[ true , "" ], 149[ false , "first" ], 150]); 151const first = await shown ( root , working . log [ 1 ]. route ); 152assertEquals ( first . revision . refs . some (( ref ) => ref . includes ( "HEAD" )), true ); 153assertEquals ( first . revision . commit , { 154full :await run ( root , "git" , "rev-parse" , "HEAD" ), 155prefix :"" , 156rest :await run ( root , "git" , "rev-parse" , "--short" , "HEAD" ), 157}); 158assertEquals ( first . files . map (({ path, status}) => [ path , status ]), [ 159[ ".gitignore" , "added" ], 160[ "a.txt" , "added" ], 161]); 162}), 163}); 164 165Deno . test ({ 166name :"a view is sent only the keys of the changes it has already, until they change" , 167ignore :! onPath ( "git" ), 168fn :() => 169repository ( async ( root ) => { 170await run ( root , "git" , "init" , "--quiet" ); 171await writeFile ( join ( root , "a.txt" ), "one\n" ); 172await writeFile ( join ( root , "b.txt" ), "two\n" ); 173const view = testExtension ( diff , { env :localEnvironment ( root ) }); 174const ask = async ( known :string []) => 175( await view . request ( "show" , { route :"" , known}) as Reply ). files ; 176const first = await ask ([]); 177assertEquals ( first . map (({ diff}) => diff ?. path ), [ "a.txt" , "b.txt" ]); 178const known = first . map (({ key}) => key ); 179assertEquals ( await ask ( known ), known . map (( key ) => ({ key, diff :null }))); 180await writeFile ( join ( root , "b.txt" ), "three\n" ); 181assertEquals (( await ask ( known )). map (({ diff}) => diff ?. path ?? null ), [ null , "b.txt" ]); 182}), 183}); 184 185Deno . test ( "outside a repository, or given a route that names no revision, it says so" , () => 186repository ( async ( root ) => { 187await assertRejects (() => shown ( root ), Error , "neither a jj nor a git repository" ); 188if ( ! onPath ( "git" )) return ; 189await run ( root , "git" , "init" , "--quiet" ); 190for ( const route of [ "--output=/tmp/x" , "HEAD~1" , "main" ]) { 191await assertRejects (() => shown ( root , route ), Error , "Not a commit id" ); 192} 193})); 194 195Deno . test ({ 196name :"human saves report only their changes on exit, and retries do not repeat saves or reports" , 197ignore :! onPath ( "git" ), 198}, () => 199repository ( async ( root ) => { 200await run ( root , "git" , "init" , "--quiet" ); 201await writeFile ( join ( root , "a.txt" ), "base\nshared\nend\n" ); 202await run ( root , "git" , "add" , "-A" ); 203await run ( root , "git" , "commit" , "--quiet" , "-m" , "first" ); 204await writeFile ( join ( root , "a.txt" ), "agent\nshared\nend\n" ); 205const view = testExtension ( diff , { env :localEnvironment ( root ) }); 206const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 207const save = saving ( opened , "agent\nhuman\nend\n" ); 208const [ saved , duplicate ] = await Promise . all ([ 209view . request ( "save" , save ), 210view . request ( "save" , save ), 211]) as Saved []; 212assertEquals ( saved , duplicate ); 213assertEquals ( await view . prompt (), "" ); 214assertEquals ( view . messages , []); 215assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), save . content ); 216await writeFile ( join ( root , "other.txt" ), "later agent edit\n" ); 217await view . command ( 218"diff-finish" , 219JSON . stringify ({ session :"review" , saves :[ saved ] }), 220); 221const messages = view . messages ; 222assertEquals ( messages . length , 1 ); 223const report = messages [ 0 ]. content ; 224assert ( typeof report === "string" ); 225assertStringIncludes ( report , "+human" ); 226assert ( 227! report . includes ( "-base" ) && ! report . includes ( "+agent" ) && ! report . includes ( "other.txt" ), 228); 229assertEquals ( await view . request ( "save" , save ), saved ); 230await view . command ( "diff-finish" , "review" ); 231assertEquals ( await view . prompt (), "" ); 232assertEquals ( view . messages , messages ); 233 234const next = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 235const secondSave = await view . request ( "save" , { 236 ...saving ( next , "agent\nhuman again\nend\n" ), 237session :"second" , 238}) as Saved ; 239await view . command ( 240"diff-finish" , 241JSON . stringify ({ session :"second" , saves :[ secondSave ] }), 242); 243assertEquals ( view . messages . slice ( 0 , messages . length ), messages ); 244const second = view . messages [ 1 ]. content ; 245assert ( typeof second === "string" ); 246assertStringIncludes ( second , "+human again" ); 247assertEquals ( await view . prompt (), "" ); 248 249const unchanged = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 250const unchangedSave = await view . request ( "save" , { 251 ...saving ( unchanged , unchanged . content ), 252session :"unchanged" , 253}) as Saved ; 254await view . command ( 255"diff-finish" , 256JSON . stringify ({ session :"unchanged" , saves :[ unchangedSave ] }), 257); 258assertEquals ( view . messages . length , 2 ); 259})); 260 261Deno . test ({ 262name : 263"jj edits use complete files even when the displayed patch omits lines, preserving executable CRLF text" , 264ignore :! onPath ( "jj" ), 265}, () => 266repository ( async ( root ) => { 267await run ( root , "jj" , "git" , "init" ); 268const path = "script.txt" ; 269const baseline = "\ufeff" + Array . from ({ length :21_000 }, ( _ , i ) => `line ${ i } ` ). join ( "\r\n" ); 270await writeFile ( join ( root , path ), baseline ); 271await chmod ( join ( root , path ), 0o755 ); 272await run ( root , "jj" , "commit" , "-m" , "first" ); 273const content = baseline . replace ( "line 10000\r" , "agent\r" ); 274await writeFile ( join ( root , path ), content ); 275const view = testExtension ( diff , { env :localEnvironment ( root ) }); 276const display = await view . request ( "show" , {}) as Reply ; 277assert ( display . files [ 0 ]. diff ! . hunks . flatMap (( h ) => h . lines ). length < 30 ); 278const opened = await view . request ( "open" , { root, path}) as OpenFile ; 279assertEquals ( opened . original , baseline ); 280assertEquals ( opened . content , content ); 281const edited = content . replace ( "line 20000\r" , "human\r" ); 282await view . request ( "save" , saving ( opened , edited )); 283assertEquals ( await readFile ( join ( root , path ), "utf8" ), edited ); 284assertEquals (( await Deno . stat ( join ( root , path ))). mode ! & 0o777 , 0o755 ); 285})); 286 287Deno . test ({ 288name :"a stale file or a different jj working-copy change refuses a save without overwriting" , 289ignore :! onPath ( "jj" ), 290}, () => 291repository ( async ( root ) => { 292await run ( root , "jj" , "git" , "init" ); 293await writeFile ( join ( root , "a.txt" ), "initial\n" ); 294const view = testExtension ( diff , { env :localEnvironment ( root ) }); 295const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 296const save = saving ( opened , "human\n" ); 297await writeFile ( join ( root , "a.txt" ), "external\n" ); 298await assertRejects ( 299() => view . request ( "save" , save ), 300Error , 301"changed on disk" , 302); 303assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "external\n" ); 304await writeFile ( join ( root , "a.txt" ), opened . content ); 305await run ( root , "jj" , "new" ); 306await assertRejects ( 307() => view . request ( "save" , save ), 308Error , 309"working copy changed" , 310); 311assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), opened . content ); 312assertEquals ( await view . prompt (), "" ); 313})); 314 315Deno . test ({ 316name : 317"diff saves cannot escape the repository, edit metadata, or follow a replaced symbolic link" , 318ignore :! onPath ( "git" ), 319}, () => 320repository ( async ( root ) => { 321await run ( root , "git" , "init" , "--quiet" ); 322await writeFile ( join ( root , "a.txt" ), "initial\n" ); 323const view = testExtension ( diff , { env :localEnvironment ( root ) }); 324const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 325const save = saving ( opened , "human\n" , "../outside" ); 326await assertRejects ( 327() => view . request ( "save" , save ), 328Error , 329"repository path" , 330); 331await assertRejects ( 332() => view . request ( "save" , { ...save , path :".git/config" }), 333Error , 334"repository path" , 335); 336await usingoutside = await tempDir (); 337const target = join ( outside . path , "target.txt" ); 338await writeFile ( target , opened . content ); 339await unlink ( join ( root , "a.txt" )); 340await symlink ( target , join ( root , "a.txt" )); 341await assertRejects ( 342() => view . request ( "save" , { ...save , path :"a.txt" }), 343Error , 344"symbolic links" , 345); 346assertEquals ( await readFile ( target , "utf8" ), opened . content ); 347})); 348 349Deno . test ({ 350name :"git merge conflicts are editable, and a save resolves the markers on disk" , 351ignore :! onPath ( "git" ), 352}, () => 353repository ( async ( root ) => { 354await run ( root , "git" , "init" , "--initial-branch=main" , "--quiet" ); 355await writeFile ( join ( root , "a.txt" ), "base\n" ); 356await run ( root , "git" , "add" , "-A" ); 357await run ( root , "git" , "commit" , "--quiet" , "-m" , "base" ); 358await run ( root , "git" , "checkout" , "-qb" , "branch" ); 359await writeFile ( join ( root , "a.txt" ), "branch\n" ); 360await run ( root , "git" , "commit" , "-am" , "branch" , "--quiet" ); 361await run ( root , "git" , "checkout" , "main" , "--quiet" ); 362await writeFile ( join ( root , "a.txt" ), "main\n" ); 363await run ( root , "git" , "commit" , "-am" , "main" , "--quiet" ); 364await assertRejects (() => run ( root , "git" , "merge" , "branch" )); 365const view = testExtension ( diff , { env :localEnvironment ( root ) }); 366const current = await view . request ( "show" , {}) as Reply ; 367assertEquals ( current . revision . conflict , true ); 368const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 369assertStringIncludes ( opened . content , "<<<<<<<" ); 370assertEquals ( opened . original , "main\n" ); 371await view . request ( "save" , saving ( opened , "resolved\n" )); 372assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "resolved\n" ); 373})); 374 375/** A jj repository whose commit "theirs" changes a.txt the way "ours" does, rebased onto it to conflict. */ 376async function conflicting ( root :string ) { 377await run ( root , "jj" , "git" , "init" ); 378await writeFile ( join ( root , "a.txt" ), "one\ntwo\nthree\n" ); 379await run ( root , "jj" , "describe" , "-m" , "base" ); 380await run ( root , "jj" , "new" ); 381await writeFile ( join ( root , "a.txt" ), "ONE\ntwo\nthree\n" ); 382await run ( root , "jj" , "describe" , "-m" , "ours" ); 383const ours = await run ( root , "jj" , "log" , "--no-graph" , "-r" , "@" , "-T" , "change_id" ); 384await run ( root , "jj" , "new" , "root()" ); 385await writeFile ( join ( root , "a.txt" ), "one\ntwo\nTHREE\n" ); 386await run ( root , "jj" , "describe" , "-m" , "theirs" ); 387const theirs = await run ( root , "jj" , "log" , "--no-graph" , "-r" , "@" , "-T" , "change_id" ); 388await run ( root , "jj" , "new" , ours ); 389await run ( root , "jj" , "rebase" , "-s" , theirs , "-d" , ours ); 390return { ours, theirs}; 391} 392 393Deno . test ({ 394name :"jj: a conflicted commit's file can be edited, resolving its conflict" , 395ignore :! onPath ( "jj" ), 396}, () => 397repository ( async ( root ) => { 398const { theirs} = await conflicting ( root ); 399const revision = ( await shown ( root , theirs )). revision ; 400assertEquals ([ revision . route , revision . conflict ], [ theirs , true ]); 401const view = testExtension ( diff , { env :localEnvironment ( root ) }); 402const opened = await view . request ( "open" , { 403 root, 404path :"a.txt" , 405route :theirs , 406}) as OpenFile ; 407assertStringIncludes ( opened . content , "<<<<<<<" ); 408assertEquals ( opened . original , "ONE\ntwo\nthree\n" ); 409const saved = await view . request ( "save" , saving ( opened , "ONE\ntwo\nTHREE\n" )) as Saved ; 410assert ( saved . working . identity !== opened . working . identity ); 411assertEquals ( 412await run ( root , "jj" , "file" , "show" , "-r" , theirs , "--" , "a.txt" ), 413"ONE\ntwo\nTHREE" , 414); 415assertEquals ( await run ( root , "jj" , "log" , "--no-graph" , "-r" , "conflicts()" ), "" ); 416})); 417 418Deno . test ({ 419name :"jj: a conflicted working copy's file can be edited, resolving its conflict" , 420ignore :! onPath ( "jj" ), 421}, () => 422repository ( async ( root ) => { 423const { theirs} = await conflicting ( root ); 424await run ( root , "jj" , "edit" , theirs ); 425const view = testExtension ( diff , { env :localEnvironment ( root ) }); 426const current = await shown ( root ); 427assertEquals ([ current . revision . workingCopy , current . revision . conflict ], [ true , true ]); 428const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 429assertStringIncludes ( opened . content , "<<<<<<<" ); 430await view . request ( "save" , saving ( opened , "ONE\ntwo\nTHREE\n" )); 431assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "ONE\ntwo\nTHREE\n" ); 432assertEquals ( await run ( root , "jj" , "log" , "--no-graph" , "-r" , "conflicts()" ), "" ); 433})); 434 435Deno . test ({ 436name : 437"an exit waits for an accepted save even when its receipt was not in the exit's starting state" , 438ignore :! onPath ( "git" ), 439}, () => 440repository ( async ( root ) => { 441await run ( root , "git" , "init" , "--quiet" ); 442await writeFile ( join ( root , "a.txt" ), "initial\n" ); 443const env = localEnvironment ( root ); 444const entered = Promise . withResolvers < void >(); 445const release = Promise . withResolvers < void >(); 446const write = env . writeFile . bind ( env ); 447env . writeFile = async ( ...args ) => { 448entered . resolve (); 449await release . promise ; 450return await write ( ...args ); 451}; 452const view = testExtension ( diff , { env}); 453const opened = await view . request ( "open" , { root, path :"a.txt" }) as OpenFile ; 454const saved = view . request ( "save" , saving ( opened , "human\n" )); 455try { 456await entered . promise ; 457const finishing = view . command ( "diff-finish" , "review" ); 458let finished = false ; 459void finishing . then (() => { 460finished = true ; 461}); 462await Promise . resolve (); 463await Promise . resolve (); 464assertEquals ( finished , false ); 465release . resolve (); 466await saved ; 467await finishing ; 468const report = view . messages [ 0 ]?. content ; 469assert ( typeof report === "string" ); 470assertStringIncludes ( report , "+human" ); 471} finally { 472release . resolve (); 473await saved ; 474} 475})); 476 477Deno . test ({ 478name : 479"a jj commit's file can be edited, rebasing its descendants, and a save that would conflict them is undone unless forced" , 480ignore :! onPath ( "jj" ), 481}, () => 482repository ( async ( root ) => { 483await run ( root , "jj" , "git" , "init" ); 484await writeFile ( join ( root , "a.txt" ), "one\ntwo\nthree\nfour\nfive\n" ); 485await run ( root , "jj" , "commit" , "-m" , "first" ); 486await writeFile ( join ( root , "a.txt" ), "one\ntwo\nthree\nfour\nFIVE\n" ); 487await run ( root , "jj" , "commit" , "-m" , "second" ); 488await writeFile ( join ( root , "b.txt" ), "working\n" ); 489const view = testExtension ( diff , { env :localEnvironment ( root ) }); 490const first = ( await shown ( root )). log . find (( r ) => r . description . startsWith ( "first" )) ! ; 491const opened = await view . request ( "open" , { 492 root, 493path :"a.txt" , 494route :first . route , 495}) as OpenFile ; 496assertEquals ( opened . content , "one\ntwo\nthree\nfour\nfive\n" ); 497const saved = await view . request ( 498"save" , 499saving ( opened , "ONE\ntwo\nthree\nfour\nfive\n" ), 500) as Saved ; 501assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "ONE\ntwo\nthree\nfour\nFIVE\n" ); 502assertEquals ( await readFile ( join ( root , "b.txt" ), "utf8" ), "working\n" ); 503assertEquals ( await run ( root , "jj" , "log" , "--no-graph" , "-r" , "conflicts()" ), "" ); 504await assertRejects ( 505() => view . request ( "save" , { ...saving ( opened , "x\n" ), id :"stale" }), 506Error , 507"changed since you opened it" , 508); 509 510const again = { ...saving ( opened , "ONE\ntwo\nthree\nfour\nfive!\n" ), id :"conflicting" }; 511again . working = saved . working ; 512const operation = await run ( root , "jj" , "op" , "log" , "--no-graph" , "--limit=1" , "-T" , "id" ); 513await assertRejects ( 514() => view . request ( "save" , again ), 515Error , 516"would cause conflicts in 2 later commits" , 517); 518assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "ONE\ntwo\nthree\nfour\nFIVE\n" ); 519const restored = await run ( root , "jj" , "op" , "log" , "--no-graph" , "--limit=1" , "-T" , "id" ); 520assert ( restored !== operation ); 521assertEquals ( await run ( root , "jj" , "log" , "--no-graph" , "-r" , "conflicts()" ), "" ); 522await view . request ( "save" , { ...again , force :true }); 523assertStringIncludes ( await readFile ( join ( root , "a.txt" ), "utf8" ), "<<<<<<<" ); 524 525await view . command ( "diff-finish" , "review" ); 526const report = view . messages [ 0 ]?. content ; 527assert ( typeof report === "string" ); 528assertStringIncludes ( report , `"commit":" ${ first . route } "` ); 529await assertRejects ( 530() => 531view . request ( "open" , { root, path :"a.txt" , route :"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }), 532); 533})); 534 535Deno . test ({ 536name :"jj: rewording targets the opened change and preserves its files and dirty descendants" , 537ignore :! onPath ( "jj" ), 538fn :() => 539repository ( async ( root ) => { 540await run ( root , "jj" , "git" , "init" ); 541await writeFile ( join ( root , "a.txt" ), "base\n" ); 542await run ( root , "jj" , "describe" , "-m" , "original\n\nbody" ); 543const opened = await shown ( root ); 544const view = testExtension ( diff , { env :localEnvironment ( root ) }); 545await run ( root , "jj" , "new" , "-m" , "descendant" ); 546await writeFile ( join ( root , "a.txt" ), "dirty descendant\n" ); 547const change = opened . revision . change ! . full ; 548await view . request ( "reword" , { 549 root, 550 change, 551before :opened . revision . description , 552description :"--new subject\n\nnew body" , 553}); 554const updated = await shown ( root , change ); 555assertEquals ( updated . revision . description , "--new subject\n\nnew body\n" ); 556assertEquals ( updated . revision . change , opened . revision . change ); 557assertEquals ( summary ( updated ), summary ( opened )); 558const working = await shown ( root ); 559assertEquals ( working . revision . description , "descendant\n" ); 560assertEquals ( working . log [ 1 ]. change ! . full , change ); 561assertEquals ( await readFile ( join ( root , "a.txt" ), "utf8" ), "dirty descendant\n" ); 562 563await view . request ( "reword" , { 564 root, 565change :working . revision . change ! . full , 566before :working . revision . description , 567description :"" , 568}); 569assertEquals (( await shown ( root )). revision . description , "" ); 570}), 571}); 572 573Deno . test ({ 574name :"jj: rewording rejects stale descriptions, another repository, and immutable commits" , 575ignore :! onPath ( "jj" ), 576fn :() => 577repository ( async ( root ) => { 578await run ( root , "jj" , "git" , "init" ); 579await run ( root , "jj" , "describe" , "-m" , "original" ); 580const opened = await shown ( root ); 581const view = testExtension ( diff , { env :localEnvironment ( root ) }); 582const args = { 583 root, 584change :opened . revision . change ! . full , 585before :opened . revision . description , 586description :"replacement" , 587}; 588await assertRejects ( 589() => view . request ( "reword" , { ...args , root :` ${ root } /elsewhere` }), 590Error , 591"another repository" , 592); 593await run ( root , "jj" , "describe" , "-m" , "someone else's edit" ); 594await assertRejects (() => view . request ( "reword" , args ), Error , "description changed" ); 595assertEquals (( await shown ( root )). revision . description , "someone else's edit\n" ); 596await run ( root , "jj" , "bookmark" , "create" , "frozen" ); 597await run ( root , "jj" , "new" ); 598await run ( 599root , 600"jj" , 601"config" , 602"set" , 603"--repo" , 604'revset-aliases."immutable_heads()"' , 605"frozen" , 606); 607await assertRejects (() => view . request ( "reword" , args ), Error , "immutable" ); 608}), 609});