512 lines
29 KiB
Nu
512 lines
29 KiB
Nu
#!/usr/bin/env nu
|
|
# Compare a deterministic agent workflow with native Git. Fail on every difference.
|
|
def main [component: path, server: path = '/tmp/p1-git/server-target/debug/git-test-http-server'] {
|
|
let component = ($component | path expand)
|
|
let wit = (^wasm-tools component wit $component | complete)
|
|
if $wit.exit_code != 0 {error make {msg: $wit.stderr}}
|
|
let imports = ($wit.stdout | lines | str trim | where {|line| $line | str starts-with 'import '})
|
|
let forbidden = ($imports | where {|line| not ($line | str starts-with 'import wasi:')})
|
|
if ($imports | is-empty) or not ($forbidden | is-empty) {error make {msg: $'Unexpected component imports: ($imports)'}}
|
|
print 'Component imports: PASS (WASI only)'
|
|
print ($imports | str join (char newline))
|
|
let root = (mktemp -d -p /tmp/p1-git workflow.XXXXXX)
|
|
print $'Fixture: ($root)'
|
|
let server_root = ($root | path join server)
|
|
mkdir $server_root
|
|
let ready = ($root | path join port)
|
|
let server_job = (job spawn { ^$server $server_root $ready })
|
|
let deadline = ((date now) + 10sec)
|
|
while (not ($ready | path exists)) or (open --raw $ready | str trim | is-empty) { if (date now) > $deadline { job kill $server_job; error make {msg: 'HTTP fixture did not start'} } }
|
|
let url = $'http://127.0.0.1:(open --raw $ready | str trim)/coord.git'
|
|
try {
|
|
let envs = {GIT_AUTHOR_NAME: 'Agent', GIT_AUTHOR_EMAIL: 'agent@example.test', GIT_COMMITTER_NAME: 'Agent', GIT_COMMITTER_EMAIL: 'agent@example.test', GIT_AUTHOR_DATE: '1700000000 +0000', GIT_COMMITTER_DATE: '1700000000 +0000', GIT_CONFIG_NOSYSTEM: '1', GIT_EDITOR: 'true', HOME: '/tmp/p1-git/empty-home', LC_ALL: 'C', TZ: 'UTC'}
|
|
mkdir $envs.HOME
|
|
with-env $envs {
|
|
for side in [native wasi] { mkdir ($root | path join $side repo) }
|
|
let steps = [
|
|
{args: [init -q -b main], compare: true}
|
|
{args: [config user.name Agent], compare: true}
|
|
{args: [config user.email agent@example.test], compare: true}
|
|
{args: [-c user.name=Override config --get user.name], compare: true}
|
|
{args: [config --get user.name], compare: true}
|
|
{write: 'base\n', file: 'file.txt'}
|
|
{args: [add .], compare: true}
|
|
{args: [commit -m initial], compare: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [status -sb], compare: true}
|
|
{args: [status --porcelain=v1], compare: true}
|
|
{args: [status], compare: true}
|
|
{args: [ls-files], compare: true}
|
|
{publish: true}
|
|
{args: [remote add fixture $url], compare: true, state: true}
|
|
{args: [fetch fixture main], compare: true, state: true}
|
|
{args: [rev-parse FETCH_HEAD], compare: true}
|
|
{args: [rev-parse --verify --quiet HEAD], compare: true}
|
|
{args: [rev-parse --verify --quiet absent], compare: true, exit: 1}
|
|
{args: [rev-parse --abbrev-ref HEAD], compare: true}
|
|
{args: [symbolic-ref HEAD], compare: true}
|
|
{args: [symbolic-ref --quiet --short HEAD], compare: true}
|
|
{args: [branch --list main], compare: true}
|
|
{args: [branch --list absent], compare: true}
|
|
{args: [worktree add --detach ../detached HEAD], compare: true, state: true}
|
|
{args: [worktree list --porcelain], compare: true, normalize_paths: true}
|
|
{remove: '../detached'}
|
|
{args: [worktree prune], compare: true, state: true}
|
|
{args: [worktree list --porcelain], compare: true, normalize_paths: true}
|
|
{args: [branch --show-current], compare: true}
|
|
{args: [branch -a], compare: true}
|
|
{args: [log -n 1], compare: true}
|
|
{args: [log -1], compare: true}
|
|
{args: [log --oneline -1], compare: true}
|
|
{args: [log -n 1 '--format=%H %h %s %an %ae %ad %d'], compare: true}
|
|
{args: [log -n 1 '--pretty=format:%H %h %s %an %ae %ad %d'], compare: true}
|
|
{args: [log -n 1 -p], compare: true}
|
|
{args: [log -n 1 --stat], compare: true}
|
|
{args: [log --oneline -- file.txt], compare: true}
|
|
{args: [show --stat], compare: true}
|
|
{args: [show --oneline], compare: true}
|
|
{args: [show --name-only], compare: true}
|
|
{args: [show --oneline --stat], compare: true}
|
|
{args: [cat-file -t HEAD], compare: true}
|
|
{args: [cat-file -p HEAD], compare: true}
|
|
{args: [cat-file -s HEAD], compare: true}
|
|
{args: [cat-file -e HEAD], compare: true}
|
|
{args: [cat-file -e absent], compare: false, exit: 128}
|
|
{args: [cat-file -t 'HEAD:file.txt'], compare: true}
|
|
{args: [cat-file -p 'HEAD:file.txt'], compare: true}
|
|
{args: [cat-file -s 'HEAD:file.txt'], compare: true}
|
|
{args: [cat-file -p 'HEAD^{tree}'], compare: true}
|
|
{args: [ls-tree HEAD], compare: true}
|
|
{args: [ls-tree -r HEAD], compare: true}
|
|
{args: [ls-tree --name-only HEAD], compare: true}
|
|
{args: [ls-tree -r --name-only HEAD file.txt], compare: true}
|
|
{args: [rev-parse --short HEAD], compare: true}
|
|
{args: [show HEAD], compare: true}
|
|
{args: [show HEAD:file.txt], compare: true}
|
|
{args: [branch topic], compare: true}
|
|
{args: [switch topic], compare: true}
|
|
{write: 'topic\n', file: 'topic.txt'}
|
|
{args: [add .], compare: true}
|
|
{args: [commit -m topic], compare: true}
|
|
{args: [rev-parse HEAD HEAD~1], compare: true}
|
|
{args: [log -1], compare: true}
|
|
{args: [log -2], compare: true}
|
|
{args: [log -n 2], compare: true}
|
|
{args: [log --oneline -5], compare: true}
|
|
{args: [log --max-count=1 --oneline], compare: true}
|
|
{args: [diff HEAD~1], compare: true}
|
|
{args: [diff HEAD~1..HEAD], compare: true}
|
|
{args: [merge-base HEAD~1 HEAD], compare: true}
|
|
{args: [merge-base --is-ancestor HEAD~1 HEAD], compare: true}
|
|
{args: [merge-base --is-ancestor HEAD HEAD~1], compare: true, exit: 1}
|
|
{args: [rev-list --count HEAD~1..HEAD], compare: true}
|
|
{args: [rev-list --count HEAD..HEAD], compare: true}
|
|
{args: [log --oneline HEAD~1..HEAD], compare: true}
|
|
{args: [log --name-only --no-renames --diff-merges=first-parent '--format=%x1e%H%x00%s%x00%(trailers:key=Sausage-Conversation,valueonly)%x00' HEAD~1..HEAD], compare: true}
|
|
{args: [diff --name-only HEAD~1 HEAD], compare: true}
|
|
{args: [diff --name-only -z HEAD~1 HEAD], compare: true}
|
|
{args: [diff --numstat --no-renames HEAD~1 HEAD], compare: true}
|
|
{args: [diff --numstat --no-renames -z HEAD~1 HEAD], compare: true}
|
|
{args: [update-ref refs/heads/transaction HEAD], compare: true, state: true}
|
|
{args: [update-ref refs/heads/transaction HEAD~1 HEAD], compare: true, state: true}
|
|
{args: [update-ref refs/heads/transaction HEAD HEAD], compare: true, exit: 128, state: true}
|
|
{args: [branch -D transaction], compare: true, state: true}
|
|
{args: [diff --name-only HEAD~1..HEAD], compare: true}
|
|
{args: [diff --name-status HEAD~1..HEAD], compare: true}
|
|
{args: [diff HEAD~1 -- topic.txt], compare: true}
|
|
{args: [branch disposable], compare: true}
|
|
{args: [branch -d disposable], compare: true}
|
|
{args: [branch disposable], compare: true}
|
|
{args: [branch -D disposable], compare: true}
|
|
{args: [branch disposable], compare: true}
|
|
{args: [branch -q -d disposable], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{args: [merge --ff-only topic], compare: true, state: true}
|
|
{write: 'changed\n', file: 'file.txt'}
|
|
{args: [status --short], compare: true}
|
|
{args: [diff], compare: true}
|
|
{args: [diff --stat], compare: true}
|
|
{args: [status], compare: true}
|
|
{args: [commit -am changed], compare: true}
|
|
{args: [log --oneline], compare: true}
|
|
{args: [switch topic], compare: true}
|
|
{write: 'conflict\n', file: 'file.txt'}
|
|
{args: [commit -am conflict], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{args: [merge topic], compare: true, exit: 1}
|
|
{args: [diff --diff-filter=U --name-only], compare: true}
|
|
{args: [diff --diff-filter=U --name-only -z], compare: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [merge --abort], compare: true}
|
|
{args: [merge -q topic], compare: true, exit: 1}
|
|
{args: [merge --abort], compare: true}
|
|
{args: [worktree add -b linked ../linked], compare: true}
|
|
{args: [worktree list], compare: false}
|
|
{args: [worktree remove ../linked], compare: true}
|
|
{args: [switch topic], compare: true}
|
|
{args: [rebase main], compare: true, exit: 1}
|
|
{write: 'resolved\n', file: 'file.txt'}
|
|
{args: [add file.txt], compare: true}
|
|
{args: [rebase --continue], compare: true}
|
|
{args: [rebase -q main], compare: true}
|
|
{args: [log --oneline], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{write: 'left\n', file: 'left.txt'}
|
|
{args: [add .], compare: true}
|
|
{args: [commit -m left], compare: true}
|
|
{args: [switch topic], compare: true}
|
|
{write: 'right\n', file: 'right.txt'}
|
|
{args: [add .], compare: true}
|
|
{args: [commit -m right], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{args: [merge topic], compare: true}
|
|
{args: [show HEAD], compare: true}
|
|
{args: [branch], compare: true}
|
|
{args: [rm right.txt], compare: true}
|
|
{args: [diff --cached], compare: true}
|
|
{args: [status --porcelain], compare: true}
|
|
{args: [commit -m removed], compare: true}
|
|
{args: [checkout HEAD~1], compare: true}
|
|
{args: [status], compare: true}
|
|
{args: [status -sb], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{args: [checkout -q -b agent-checkout], compare: true}
|
|
{args: [switch -q main], compare: true}
|
|
{args: [switch -q -c agent-switch], compare: true}
|
|
{write: 'agent\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [commit -q -m agent], compare: true}
|
|
{args: [switch main], compare: true}
|
|
{args: [merge --no-ff -q --no-edit -m landed agent-switch], compare: true, state: true}
|
|
{args: [log --name-only --no-renames --diff-merges=first-parent '--format=%x1e%H%x00%s%x00%(trailers:key=Sausage-Conversation,valueonly)%x00' HEAD~1..HEAD], compare: true}
|
|
{args: [merge --ff-only agent-switch], compare: true, state: true}
|
|
{args: [show --stat], compare: true}
|
|
{args: [commit --quiet --allow-empty -m quiet], compare: true}
|
|
{args: [init], compare: true, normalize_path: true, state: true}
|
|
{args: [init -q], compare: true, state: true}
|
|
{args: [log -n 1 --oneline -- file.txt], compare: true}
|
|
{args: [log -n 2 --oneline -- file.txt], compare: true}
|
|
{args: [config branch.main.description agent-branch], compare: true}
|
|
{args: [branch -m rework], compare: true, state: true}
|
|
{args: [config --get branch.rework.description], compare: true}
|
|
{args: [branch -m agent-checkout renamed], compare: true, state: true}
|
|
{args: [branch], compare: true}
|
|
{args: [remote add example https://example.test/repo.git], compare: true, state: true}
|
|
{args: [remote -v], compare: true}
|
|
{args: [remote set-url example https://example.test/new.git], compare: true, state: true}
|
|
{args: [remote get-url example], compare: true}
|
|
{args: [remote remove example], compare: true, state: true}
|
|
{args: [remote -v], compare: true}
|
|
{args: [tag lightweight], compare: true, state: true}
|
|
{args: [tag -a annotated -m annotation], compare: true, state: true}
|
|
{args: [tag], compare: true}
|
|
{args: [tag --list], compare: true}
|
|
{args: [tag -d annotated], compare: true, state: true}
|
|
{args: [tag -d lightweight], compare: true, state: true}
|
|
{write: 'amended\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [commit --amend -m amended], compare: true, state: true}
|
|
{args: [commit --amend --no-edit], compare: true, state: true}
|
|
{write: 'file message\n\nbody\n', file: 'message.txt'}
|
|
{write: 'from-file\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [commit -F message.txt], compare: true, state: true}
|
|
{write: 'dirty\n', file: 'agent.txt'}
|
|
{args: [checkout agent.txt], compare: true, state: true}
|
|
{write: 'dirty\n', file: 'agent.txt'}
|
|
{args: [checkout -- agent.txt], compare: true, state: true}
|
|
{write: 'dirty\n', file: 'agent.txt'}
|
|
{args: [restore agent.txt], compare: true, state: true}
|
|
{write: 'staged\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [restore --staged agent.txt], compare: true, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [restore agent.txt], compare: true, state: true}
|
|
{write: 'staged\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [reset HEAD agent.txt], compare: true, state: true}
|
|
{args: [restore agent.txt], compare: true, state: true}
|
|
{args: [branch saved], compare: true}
|
|
{args: [reset --soft HEAD~1], compare: true, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [log --oneline -n 2], compare: true}
|
|
{args: [reset --hard saved], compare: true, state: true}
|
|
{args: [reset HEAD~1], compare: true, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [reset --hard saved], compare: true, state: true}
|
|
{args: [reset --hard HEAD~1], compare: true, state: true}
|
|
{args: [reset --hard saved], compare: true, state: true}
|
|
{write: 'stash one\n', file: 'agent.txt'}
|
|
{args: [stash push], compare: true, state: true}
|
|
{args: [stash list], compare: true}
|
|
{args: [stash bogus], compare: true, exit: 128, state: true}
|
|
{args: [stash apply], compare: true, state: true}
|
|
{args: [reset --hard], compare: true, state: true}
|
|
{args: [stash drop], compare: true, state: true}
|
|
{write: 'stash two\n', file: 'agent.txt'}
|
|
{args: [stash -m custom], compare: true, state: true}
|
|
{args: [stash list], compare: true}
|
|
{write: 'stash three\n', file: 'agent.txt'}
|
|
{args: [stash], compare: true, state: true}
|
|
{args: [stash list], compare: true}
|
|
{args: [stash drop], compare: true, state: true}
|
|
{args: [stash pop], compare: true, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [reset --hard], compare: true, state: true}
|
|
{args: [stash push], compare: true, state: true}
|
|
{write: 'added stash\n', file: 'stash-added.txt'}
|
|
{args: [add stash-added.txt], compare: true}
|
|
{write: 'stage stash\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{write: 'unstaged stash\n', file: 'agent.txt'}
|
|
{args: [stash push -m staged], compare: true, state: true}
|
|
{args: [stash apply], compare: true, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [reset --hard], compare: true, state: true}
|
|
{args: [stash drop], compare: true, state: true}
|
|
{write: 'quiet stash\n', file: 'agent.txt'}
|
|
{args: [stash push -q -m quiet], compare: true, state: true}
|
|
{args: [stash pop -q], compare: true, state: true}
|
|
{args: [stash push -q], compare: true, state: true}
|
|
{args: [stash drop -q], compare: true, state: true}
|
|
{args: [stash push -q], compare: true, state: true}
|
|
{args: [switch -c picks], compare: true}
|
|
{write: 'picked\n', file: 'pick.txt'}
|
|
{args: [add pick.txt], compare: true}
|
|
{args: [commit -m picked], compare: true}
|
|
{args: [switch rework], compare: true}
|
|
{args: [cherry-pick picks], compare: true, state: true}
|
|
{args: [log --oneline -n 2], compare: true}
|
|
{args: [switch picks], compare: true}
|
|
{write: 'theirs\n', file: 'agent.txt'}
|
|
{args: [commit -am theirs], compare: true}
|
|
{args: [switch rework], compare: true}
|
|
{write: 'ours\n', file: 'agent.txt'}
|
|
{args: [commit -am ours], compare: true}
|
|
{args: [merge-base HEAD picks], compare: true}
|
|
{args: [merge --ff-only picks], compare: true, exit: 128, state: true}
|
|
{args: [cherry-pick picks], compare: true, exit: 1, state: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [cherry-pick --abort], compare: true, state: true}
|
|
{args: [cherry-pick picks], compare: true, exit: 1, state: true}
|
|
{write: 'resolved\n', file: 'agent.txt'}
|
|
{args: [add agent.txt], compare: true}
|
|
{args: [cherry-pick --continue], compare: true, state: true}
|
|
{args: [log --oneline -n 3], compare: true}
|
|
{args: [status --short], compare: true}
|
|
{args: [cherry-pick --abort], compare: true, exit: 128, state: true}
|
|
{binary: true, file: 'binary.dat'}
|
|
{write: '+++ plus\n--- minus\n', file: 'counts.txt'}
|
|
{args: [add binary.dat counts.txt], compare: true}
|
|
{args: [diff --cached --numstat --no-renames], compare: true}
|
|
{args: [diff --cached --numstat --no-renames -z], compare: true}
|
|
{args: [reset --hard HEAD], compare: true, state: true}
|
|
{move: 'agent.txt', to: 'renamed.txt'}
|
|
{args: [add -A], compare: true}
|
|
{args: [status --porcelain -z], compare: true}
|
|
{args: [diff --cached --numstat --no-renames], compare: true}
|
|
{args: [diff --cached --numstat --no-renames -z], compare: true}
|
|
{args: [reset --hard HEAD], compare: true, state: true}
|
|
{write: 'ignored/\n*.tmp\n', file: '.gitignore'}
|
|
{args: [add .gitignore], compare: true}
|
|
{args: [commit -m ignores], compare: true}
|
|
{write: 'keep\n', file: 'ignored/keep.tmp'}
|
|
{write: 'loose\n', file: 'loose/file.txt'}
|
|
{write: 'loose\n', file: 'loose.txt'}
|
|
{args: [check-ignore --no-index ignored/keep.tmp], compare: true}
|
|
{args: [check-ignore --no-index file.txt], compare: true, exit: 1}
|
|
{args: [status --porcelain --untracked-files=all], compare: true}
|
|
{args: [status --porcelain --untracked-files=all -z], compare: true}
|
|
{args: [ls-files --others --exclude-standard], compare: true}
|
|
{args: [ls-files --others --exclude-standard -z], compare: true}
|
|
{write: 'raw path\n', file: "tab\tand\nnewline.txt"}
|
|
{args: [status --porcelain --untracked-files=all -z], compare: true}
|
|
{args: [ls-files --others --exclude-standard -z], compare: true}
|
|
{args: [clean -fdq], compare: true, state: true}
|
|
{args: [check-ignore --no-index ignored/keep.tmp], compare: true}
|
|
{write: 'dirty\n', file: 'agent.txt'}
|
|
{args: [checkout --detach --force HEAD~1], compare: true, state: true}
|
|
{args: [symbolic-ref --quiet HEAD], compare: true, exit: 1}
|
|
{args: [symbolic-ref HEAD], compare: true, exit: 128}
|
|
{args: [rev-parse --abbrev-ref HEAD], compare: true}
|
|
{args: [switch rework], compare: true}
|
|
{args: [commit --allow-empty -m trailer], native_args: [commit --allow-empty -m 'trailer\n\nSausage-Conversation: conversation-34'], conversation: conversation-34, compare: true, state: true}
|
|
{args: [log -n 1 --name-only --no-renames --diff-merges=first-parent '--format=%x1e%H%x00%s%x00%(trailers:key=Sausage-Conversation,valueonly)%x00'], compare: true}
|
|
{args: [commit --allow-empty -m no-trailer], compare: true, state: true}
|
|
{args: [log --name-only --no-renames --diff-merges=first-parent '--format=%x1e%H%x00%s%x00%(trailers:key=Sausage-Conversation,valueonly)%x00' HEAD~3..HEAD], compare: true}
|
|
{args: [diff --check], compare: true}
|
|
{args: [diff --cached --check], compare: true}
|
|
{write: 'clean\nbad \n indent\n\n', file: 'file.txt'}
|
|
{args: [diff --check], compare: true, exit: 2}
|
|
{args: [add file.txt], compare: true}
|
|
{args: [diff --cached --check], compare: true, exit: 2}
|
|
{write: 'clean\n', file: 'file.txt'}
|
|
{args: [diff --check], compare: true}
|
|
{args: [add file.txt], compare: true}
|
|
{args: [diff --cached --check], compare: true}
|
|
{write: 'clean\n indent \n<<<<<<< HEAD\n=======\n>>>>>>> other\n', file: 'file.txt'}
|
|
{args: [diff --check], compare: true, exit: 2}
|
|
{args: [status --bad-option], compare: true, exit: 129}
|
|
{args: [not-a-command], compare: true, exit: 1}
|
|
]
|
|
mut failures = 0
|
|
for step in $steps {
|
|
if 'write' in $step {
|
|
for side in [native wasi] {
|
|
let file = ($root | path join $side repo $step.file)
|
|
mkdir ($file | path dirname)
|
|
$step.write | str replace --all '\n' (char newline) | save --force $file
|
|
}
|
|
continue
|
|
}
|
|
if 'binary' in $step { for side in [native wasi] { 0x[610062] | save --force ($root | path join $side repo $step.file) }; continue }
|
|
if 'move' in $step { for side in [native wasi] { mv ($root | path join $side repo $step.move) ($root | path join $side repo $step.to) }; continue }
|
|
if 'publish' in $step {
|
|
let result = (^git clone --bare ($root | path join native repo) ($server_root | path join coord.git) | complete)
|
|
if $result.exit_code != 0 { error make {msg: $result.stderr} }
|
|
continue
|
|
}
|
|
if 'remove' in $step { for side in [native wasi] { rm -rf ($root | path join $side repo $step.remove) }; continue }
|
|
let args = $step.args
|
|
let native_args = ($step.native_args? | default $args | each {|arg| $arg | into string | str replace --all '\n' (char newline)})
|
|
let native = (^git -C ($root | path join native repo) ...$native_args | complete)
|
|
let extra_env = if 'conversation' in $step { [--env $'SAUSAGE_CONVERSATION=($step.conversation)'] } else { [] }
|
|
let wasi = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($root)/wasi::/' --env GIT_AUTHOR_NAME --env GIT_AUTHOR_EMAIL --env GIT_COMMITTER_NAME --env GIT_COMMITTER_EMAIL --env GIT_AUTHOR_DATE --env GIT_COMMITTER_DATE ...$extra_env $component -C /repo ...$args | complete)
|
|
let expected = ($step.exit? | default 0)
|
|
let native_stdout = if ($step.normalize_paths? | default false) { $native.stdout | str replace --all ($root | path join native) '' } else if ($step.normalize_path? | default false) { $native.stdout | str replace --all ($root | path join native repo) '/repo' } else { $native.stdout }
|
|
let equal = ($native.exit_code == $expected and $wasi.exit_code == $expected and (not $step.compare or $native_stdout == $wasi.stdout))
|
|
print $'($args | str join " "): (if $equal {"PASS"} else {"FAIL"}) native=($native.exit_code) wasi=($wasi.exit_code)'
|
|
if not $equal {
|
|
$failures += 1
|
|
print ({native: $native, wasi: $wasi} | to json)
|
|
}
|
|
if ($step.state? | default false) {
|
|
for check in [[status --short] [log '--format=%H %P %an %ae %s' --all] [diff] [diff --cached] [show-ref] [config --get-regexp '^(branch|remote)\.']] {
|
|
let native_state = (^git -C ($root | path join native repo) ...$check | complete)
|
|
let wasi_state = (^git -C ($root | path join wasi repo) ...$check | complete)
|
|
let same = ($native_state.exit_code == $wasi_state.exit_code and $native_state.stdout == $wasi_state.stdout)
|
|
print $' state ($check | str join " "): (if $same {"PASS"} else {"FAIL"})'
|
|
if not $same { $failures += 1; print ({native: $native_state, wasi: $wasi_state} | to json) }
|
|
}
|
|
}
|
|
}
|
|
# Explicit usage checks for documented forms that this CLI does not implement.
|
|
let unsupported = [
|
|
[log --graph --oneline --all]
|
|
]
|
|
for args in $unsupported {
|
|
let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($root)/wasi::/' $component -C /repo ...$args | complete)
|
|
let expected = 129
|
|
let equal = ($result.exit_code == $expected and $result.stdout == '' and ($result.stderr | str lowercase | str contains 'usage'))
|
|
print $'($args | str join " "): (if $equal {"PASS documented usage"} else {"FAIL documented usage"}) wasi=($result.exit_code)'
|
|
if not $equal { $failures += 1; print ($result | to json) }
|
|
}
|
|
# WASI checkout and staging retain an existing executable file's indexed mode.
|
|
let executable = ($root | path join executable)
|
|
mkdir $executable
|
|
^git -C $executable init -q -b main
|
|
"#!/bin/sh\necho executable\n" | save ($executable | path join run.sh)
|
|
^chmod +x ($executable | path join run.sh)
|
|
^git -C $executable add run.sh
|
|
^git -C $executable commit --no-gpg-sign -q -m executable
|
|
rm ($executable | path join run.sh)
|
|
mut executable_ok = true
|
|
for args in [[checkout -q HEAD -- run.sh] [status --short] [diff] [add run.sh] [diff --cached] [status --short]] {
|
|
let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($executable)::/mode' $component -C /mode ...$args | complete)
|
|
if $result.exit_code != 0 or $result.stdout != '' { $executable_ok = false; print ($result | to json) }
|
|
}
|
|
let indexed = (^git -C $executable ls-files --stage run.sh | complete)
|
|
$executable_ok = ($executable_ok and $indexed.exit_code == 0 and ($indexed.stdout | str starts-with '100755 '))
|
|
print $'executable checkout stays clean: (if $executable_ok {"PASS"} else {"FAIL"})'
|
|
if not $executable_ok { $failures += 1 }
|
|
# Status refreshes stale stats once, then detects a same-size content change.
|
|
let cached = ($root | path join cached)
|
|
mkdir $cached
|
|
^git -C $cached init -q -b main
|
|
'base' | save ($cached | path join file.txt)
|
|
^git -C $cached add file.txt
|
|
^git -C $cached commit --no-gpg-sign -q -m cached
|
|
^touch -m -d @1700000000 ($cached | path join file.txt)
|
|
let index = ($cached | path join .git/index)
|
|
let before = (open --raw $index | hash sha256)
|
|
mut cached_ok = true
|
|
mut refreshed = ''
|
|
for pass in [1 2] {
|
|
let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($cached)::/cached' $component -C /cached status --short | complete)
|
|
let current = (open --raw $index | hash sha256)
|
|
$cached_ok = ($cached_ok and $result.exit_code == 0 and $result.stdout == '')
|
|
if $pass == 1 { $cached_ok = ($cached_ok and $current != $before); $refreshed = $current } else { $cached_ok = ($cached_ok and $current == $refreshed) }
|
|
}
|
|
'edit' | save --force ($cached | path join file.txt)
|
|
let changed = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($cached)::/cached' $component -C /cached status --short | complete)
|
|
let native_changed = (^git -C $cached status --short | complete)
|
|
$cached_ok = ($cached_ok and $changed.exit_code == 0 and $changed.stdout == $native_changed.stdout and $changed.stdout == " M file.txt\n")
|
|
print $'status caches stats and detects edits: (if $cached_ok {"PASS"} else {"FAIL"})'
|
|
if not $cached_ok { $failures += 1 }
|
|
# A merge tree that differs from both parents requires a combined diff.
|
|
'combined resolution\n' | save --force ($root | path join native/repo/file.txt)
|
|
let native = ($root | path join native/repo)
|
|
^git -C $native add file.txt
|
|
let tree = (^git -C $native write-tree | str trim)
|
|
let merge = (^git -C $native commit-tree $tree -p HEAD -p HEAD~1 -m combined | str trim)
|
|
let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($root)/native/repo::/combined' $component -C /combined show $merge | complete)
|
|
let native_result = (^git -C $native show $merge | complete)
|
|
let equal = ($native_result.exit_code == 0 and $result.exit_code == 129 and $result.stdout == '' and ($result.stderr | str contains 'usage'))
|
|
print $'show combined merge: (if $equal {"PASS documented usage"} else {"FAIL documented usage"}) native=($native_result.exit_code) wasi=($result.exit_code)'
|
|
if not $equal { $failures += 1; print ($result | to json) }
|
|
# Whitespace checks read symlink text, not the target of a dangling symlink.
|
|
^ln -sfn missing ($native | path join check-link)
|
|
^git -C $native add check-link
|
|
for target in ['missing ' ' '] {
|
|
^ln -sfn $target ($native | path join check-link)
|
|
for args in [[diff --check] [add check-link] [diff --cached --check]] {
|
|
let expected = (^git -C $native ...$args | complete)
|
|
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($native)::/links' $component -C /links ...$args | complete)
|
|
let equal = ($expected.stdout == $actual.stdout and $expected.exit_code == $actual.exit_code)
|
|
print $'symlink ($args): (if $equal {"PASS"} else {"FAIL"})'
|
|
if not $equal { $failures += 1; print ($expected | to json); print ($actual | to json) }
|
|
}
|
|
}
|
|
# Reflog-only writes do not require the strict identity that new commits require.
|
|
let no_identity = ($root | path join no-identity)
|
|
mkdir $no_identity
|
|
^git -C $no_identity init -q -b main
|
|
'base' | save ($no_identity | path join file.txt)
|
|
^git -C $no_identity add file.txt
|
|
^git -C $no_identity commit --no-gpg-sign -q -m base
|
|
mkdir ($no_identity | path join nested/sub)
|
|
'nested' | save ($no_identity | path join nested/sub/file.txt)
|
|
^git -C $no_identity add .
|
|
let inspection_tree = (^git -C $no_identity write-tree | str trim)
|
|
for args in [[ls-tree $inspection_tree] [ls-tree -r $inspection_tree] [ls-tree --name-only $inspection_tree nested] [ls-tree -r --name-only $inspection_tree nested] [ls-tree $inspection_tree nested/sub/file.txt] [cat-file -p $inspection_tree]] {
|
|
let expected = (^git -C $no_identity ...$args | complete)
|
|
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo ...$args | complete)
|
|
let equal = ($expected.exit_code == 0 and $actual.exit_code == 0 and $expected.stdout == $actual.stdout)
|
|
print $'nested tree ($args): (if $equal {"PASS"} else {"FAIL"})'
|
|
if not $equal { $failures += 1; print ($expected | to json); print ($actual | to json) }
|
|
}
|
|
^git -C $no_identity reset --hard -q HEAD
|
|
^git -C $no_identity config user.useConfigOnly true
|
|
let identity_failures = (do {
|
|
mut failed = 0
|
|
hide-env -i GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL
|
|
for args in [[reset --hard HEAD] [checkout -q --detach HEAD] [checkout -q main] [branch -f other HEAD] [update-ref refs/heads/other HEAD]] {
|
|
let expected = (^git -C $no_identity ...$args | complete)
|
|
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo ...$args | complete)
|
|
let equal = ($expected.exit_code == 0 and $actual.exit_code == 0 and $expected.stdout == $actual.stdout)
|
|
print $'no identity ($args): (if $equal {"PASS"} else {"FAIL"})'
|
|
if not $equal { $failed += 1; print ($expected | to json); print ($actual | to json) }
|
|
}
|
|
let expected = (^git -C $no_identity commit --allow-empty -m missing | complete)
|
|
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo commit --allow-empty -m missing | complete)
|
|
if $expected.exit_code != 128 or $actual.exit_code != 128 { $failed += 1; print ($expected | to json); print ($actual | to json) }
|
|
print 'no identity commit requires identity: checked'
|
|
$failed
|
|
})
|
|
$failures += $identity_failures
|
|
if $failures > 0 {error make {msg: $'($failures) workflow comparisons failed'}}
|
|
}
|
|
} catch {|err| job kill $server_job; error make {msg: $err.msg} }
|
|
job kill $server_job
|
|
}
|