You've already forked guix-tribes
7dec823794
Source: guix-tribes master2ea4cae872Base: previous supertest-dev4fee530b68Mode: tree sync, preserving dev channel authorization
531 lines
25 KiB
Scheme
531 lines
25 KiB
Scheme
(define-module (tests tribes-deploy-operations)
|
|
#:use-module (ice-9 ftw)
|
|
#:use-module (guix build utils)
|
|
#:use-module (json)
|
|
#:use-module (srfi srfi-1)
|
|
#:use-module (srfi srfi-13)
|
|
#:use-module (srfi srfi-64)
|
|
#:use-module (tests support)
|
|
#:use-module (tribes deploy config)
|
|
#:use-module (tribes deploy executor)
|
|
#:use-module (tribes deploy guix-helper)
|
|
#:use-module (tribes deploy json)
|
|
#:use-module (tribes deploy operations)
|
|
#:use-module (tribes deploy plan)
|
|
#:use-module (tribes deploy state)
|
|
#:export (run-tests))
|
|
|
|
(define *fixture-counter* 0)
|
|
|
|
(define plan-a
|
|
'(("plan_hash" . "plan-a")
|
|
("resolved_plugins" . ((("name" . "aether"))))))
|
|
|
|
(define plan-b
|
|
'(("plan_hash" . "plan-b")
|
|
("resolved_plugins" . ((("name" . "aether"))))))
|
|
|
|
(define plan-without-channel-delta
|
|
'(("plan_hash" . "plan-without-channel-delta")
|
|
("resolved_channels" . #())
|
|
("resolved_plugins" . ((("name" . "supertest"))))))
|
|
|
|
(define plan-with-channel-pin
|
|
'(("plan_hash" . "plan-with-channel-pin")
|
|
("resolved_channels" . #((("channel_id" . "guix-tribes")
|
|
("name" . "tribes")
|
|
("url" . "https://git.example.test/guix-tribes.git")
|
|
("branch" . "master")
|
|
("commit" . "abc123")
|
|
("position" . 10))))
|
|
("resolved_plugins" . ((("name" . "supertest"))))))
|
|
(define plan-with-branch-channel
|
|
'(("plan_hash" . "plan-with-branch-channel")
|
|
("resolved_channels" . #((("channel_id" . "guix-tribes")
|
|
("name" . "tribes")
|
|
("url" . "https://git.example.test/guix-tribes.git")
|
|
("branch" . "master")
|
|
("commit" . #f)
|
|
("position" . 10))))
|
|
("resolved_plugins" . ((("name" . "supertest"))))))
|
|
|
|
(define (delete-if-present path)
|
|
(when (false-if-exception (lstat path))
|
|
(delete-file path)))
|
|
|
|
(define (delete-directory-if-present path)
|
|
(when (false-if-exception (lstat path))
|
|
(delete-file-recursively path)))
|
|
|
|
(define (ensure-directory path) (mkdir-p path))
|
|
|
|
(define (fresh-root)
|
|
(set! *fixture-counter* (+ *fixture-counter* 1))
|
|
(let ((root (string-append "/tmp/tribes-operations-test-"
|
|
(number->string *fixture-counter*))))
|
|
(delete-directory-if-present root)
|
|
(ensure-directory root)
|
|
root))
|
|
|
|
(define (helper-success-result payload)
|
|
((@@ (tribes deploy guix-helper) helper-success) payload '()))
|
|
|
|
(define (helper-failure-result code message)
|
|
((@@ (tribes deploy guix-helper) helper-failure) code message '() '()))
|
|
|
|
;; Build a fake helper backend that imitates the broker's view of the world.
|
|
;; The build step writes a fake store-path symlink so the broker can
|
|
;; canonicalize the GC root link the way the real helper would.
|
|
(define* (make-fake-helper fixture #:key (diverge-running? #f))
|
|
(let ((build-count 0)
|
|
(pull-count 0)
|
|
(switch-count 0)
|
|
(store-directory (assq-ref fixture 'store-directory))
|
|
(profiles-directory (assq-ref fixture 'profiles-directory))
|
|
(system-profile-link (assq-ref fixture 'system-profile-link))
|
|
(current-system-link (assq-ref fixture 'current-system-link)))
|
|
(let ((backend
|
|
(make-helper-backend
|
|
(lambda (_cfg _on-frame)
|
|
(helper-success-result
|
|
'(("event" . "done")
|
|
("catalog" . (("schemaVersion" . "2") ("plugins" . #()))))))
|
|
(lambda (_cfg _on-frame)
|
|
(set! pull-count (+ pull-count 1))
|
|
(helper-success-result
|
|
'(("event" . "done")
|
|
("phase" . "pulling")
|
|
("channels" . ((("name" . "tribes")
|
|
("url" . "https://git.example.test/guix-tribes.git")
|
|
("branch" . "master")
|
|
("commit" . "pulled456")))))))
|
|
(lambda (_cfg root-link _on-frame)
|
|
(set! build-count (+ build-count 1))
|
|
(let ((store-path
|
|
(string-append store-directory "/system-"
|
|
(number->string build-count))))
|
|
(ensure-directory store-path)
|
|
(delete-if-present root-link)
|
|
(symlink store-path root-link)
|
|
(helper-success-result
|
|
`(("event" . "done")
|
|
("store_path" . ,store-path)))))
|
|
(lambda (_cfg generation-number _on-frame)
|
|
(let* ((generation-link
|
|
(string-append profiles-directory
|
|
"/system-"
|
|
(number->string generation-number)
|
|
"-link"))
|
|
(store-path (false-if-exception
|
|
(canonicalize-path generation-link))))
|
|
(cond
|
|
((and store-path (file-exists? store-path))
|
|
(set! switch-count (+ switch-count 1))
|
|
(delete-if-present system-profile-link)
|
|
(symlink generation-link system-profile-link)
|
|
(unless diverge-running?
|
|
(delete-if-present current-system-link)
|
|
(symlink generation-link current-system-link))
|
|
(helper-success-result
|
|
`(("event" . "done")
|
|
("generation_number" . ,generation-number))))
|
|
(else
|
|
(helper-failure-result
|
|
"switch_failed"
|
|
"fake helper: missing store path"))))))))
|
|
(values backend
|
|
(lambda () build-count)
|
|
(lambda () pull-count)
|
|
(lambda () switch-count)))))
|
|
|
|
(define (write-json-file path payload)
|
|
(call-with-output-file path
|
|
(lambda (port)
|
|
(scm->json (json-ready payload) port))))
|
|
|
|
(define (read-text-file path)
|
|
(call-with-input-file path
|
|
(lambda (port)
|
|
(let loop ((chars '()))
|
|
(let ((char (read-char port)))
|
|
(if (eof-object? char)
|
|
(list->string (reverse chars))
|
|
(loop (cons char chars))))))))
|
|
|
|
(define (make-fixture)
|
|
(let* ((root (fresh-root))
|
|
(deploy-directory (string-append root "/deploy"))
|
|
(etc-directory (string-append root "/etc/tribes"))
|
|
(profiles-directory (string-append root "/profiles"))
|
|
(store-directory (string-append root "/store"))
|
|
(current-system-directory (string-append root "/current-system"))
|
|
(host-config-file (string-append etc-directory "/host-config.json"))
|
|
(channels-file (string-append etc-directory "/channels.scm"))
|
|
(current-config-file (string-append current-system-directory
|
|
"/configuration.scm"))
|
|
(current-system-link (string-append root "/run-current-system"))
|
|
(system-profile-link (string-append profiles-directory "/system")))
|
|
(ensure-directory deploy-directory)
|
|
(ensure-directory etc-directory)
|
|
(ensure-directory profiles-directory)
|
|
(ensure-directory store-directory)
|
|
(ensure-directory current-system-directory)
|
|
(write-json-file host-config-file
|
|
'(("schemaVersion" . "1")
|
|
("tribes" . (("host" . "example.test")
|
|
("plugins" . ())))))
|
|
(call-with-output-file channels-file
|
|
(lambda (port) (display "()\n" port)))
|
|
(call-with-output-file current-config-file
|
|
(lambda (port) (display ";; test config\n" port)))
|
|
`((root . ,root)
|
|
(deploy-directory . ,deploy-directory)
|
|
(host-config-file . ,host-config-file)
|
|
(channels-file . ,channels-file)
|
|
(current-config-file . ,current-config-file)
|
|
(current-system-link . ,current-system-link)
|
|
(system-profile-link . ,system-profile-link)
|
|
(system-profile-directory . ,profiles-directory)
|
|
(profiles-directory . ,profiles-directory)
|
|
(store-directory . ,store-directory))))
|
|
|
|
(define (fixture->config fixture)
|
|
(deploy-config
|
|
(deploy-directory (assq-ref fixture 'deploy-directory))
|
|
(host-config-file (assq-ref fixture 'host-config-file))
|
|
(channels-file (assq-ref fixture 'channels-file))
|
|
(current-config-file (assq-ref fixture 'current-config-file))
|
|
(current-system-link (assq-ref fixture 'current-system-link))
|
|
(system-profile-link (assq-ref fixture 'system-profile-link))
|
|
(system-profile-directory (assq-ref fixture 'system-profile-directory))
|
|
(helper-binary "fake-helper-not-used")))
|
|
|
|
(define (no-frame _) #t)
|
|
|
|
(define rollback-herd-command
|
|
(@@ (tribes deploy operations) rollback-herd-command))
|
|
|
|
(define write-plan-channels!
|
|
(@@ (tribes deploy operations) write-plan-channels!))
|
|
|
|
(define (write-executable path content)
|
|
(call-with-output-file path
|
|
(lambda (port) (display content port)))
|
|
(chmod path #o755))
|
|
|
|
(define (with-fake-rollback-herd fixture thunk)
|
|
(let* ((herd (string-append (assq-ref fixture 'root) "/fake-herd"))
|
|
(log-file (string-append (assq-ref fixture 'root) "/herd.log")))
|
|
(write-executable
|
|
herd
|
|
(string-append "#!/bin/sh\n"
|
|
"printf '%s\\n' \"$*\" >> " log-file "\n"
|
|
"exit 0\n"))
|
|
(parameterize ((rollback-herd-command herd))
|
|
(thunk))))
|
|
|
|
(define (find-generation-by-plan-hash generations plan-hash)
|
|
(find (lambda (generation)
|
|
(equal? (json-ref generation "plan_hash") plan-hash))
|
|
generations))
|
|
|
|
(define (seed-profile-generation! fixture generation-number store-name)
|
|
(let* ((store-path (string-append (assq-ref fixture 'store-directory)
|
|
"/" store-name))
|
|
(generation-link (string-append (assq-ref fixture 'profiles-directory)
|
|
"/system-"
|
|
(number->string generation-number)
|
|
"-link")))
|
|
(ensure-directory store-path)
|
|
(delete-if-present generation-link)
|
|
(symlink store-path generation-link)
|
|
store-path))
|
|
|
|
(define (run-tests)
|
|
(test-begin "tribes-deploy-operations")
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(config (fixture->config fixture))
|
|
(channels-file (assq-ref fixture 'channels-file))
|
|
(plan `(("plan_hash" . "plan-with-plugin-channel")
|
|
("resolved_channels" . ,(vector '(("channel_id" . "guix-tribes")
|
|
("name" . "tribes")
|
|
("url" . "https://git.example.test/guix-tribes.git")
|
|
("branch" . "supertest-dev")
|
|
("commit" . "plugin-commit")
|
|
("position" . 10)))))))
|
|
(call-with-output-file channels-file
|
|
(lambda (port)
|
|
(display "(list\n" port)
|
|
(display " (channel (name 'guix) (url \"https://git.example.test/guix.git\") (branch \"master\") (commit \"guix-commit\"))\n" port)
|
|
(display ")\n" port)))
|
|
(write-plan-channels! config plan)
|
|
(let ((channels (read-text-file channels-file)))
|
|
(test-assert "plan channel writer preserves current guix channel"
|
|
(string-contains channels "(name (quote guix))"))
|
|
(test-assert "plan channel writer includes plugin channel"
|
|
(string-contains channels "supertest-dev"))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((prepare-1 (prepare-plugins! state helper
|
|
(plan-plugins plan-a)
|
|
(plan-hash plan-a)
|
|
no-frame))
|
|
(prepare-2 (prepare-plugins! state helper
|
|
(plan-plugins plan-a)
|
|
(plan-hash plan-a)
|
|
no-frame)))
|
|
(test-equal "prepare returns ready"
|
|
"ready"
|
|
(json-ref prepare-1 "status"))
|
|
(test-equal "prepare is idempotent on plan_hash"
|
|
1 (get-builds))
|
|
(test-equal "idempotent prepare reuses generation number"
|
|
(json-ref prepare-1 "generation_number")
|
|
(json-ref prepare-2 "generation_number"))
|
|
(test-equal "prepare does not switch generations"
|
|
0 (get-switches))
|
|
(let* ((commit (commit-plan! state helper (plan-hash plan-a)
|
|
no-frame))
|
|
(generations (state-store-read-generations state))
|
|
(generation-a (find-generation-by-plan-hash
|
|
generations (plan-hash plan-a))))
|
|
(test-equal "commit switches after prepare"
|
|
"healthy" (json-ref commit "status"))
|
|
(test-equal "commit triggers one switch"
|
|
1 (get-switches))
|
|
(test-equal "active generation matches prepared store path"
|
|
(json-ref prepare-1 "store_path")
|
|
(state-store-running-system-path state))
|
|
(test-equal "generation is marked active after commit"
|
|
"active" (json-ref generation-a "status")))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper _get-builds _get-pulls _get-switches)
|
|
(let* ((prepared (prepare-plugins! state helper
|
|
(plan-plugins plan-with-channel-pin)
|
|
(plan-hash plan-with-channel-pin)
|
|
no-frame
|
|
#:plan plan-with-channel-pin))
|
|
(committed (commit-plan! state helper
|
|
(plan-hash plan-with-channel-pin)
|
|
no-frame))
|
|
(generation (find-generation-by-plan-hash
|
|
(state-store-read-generations state)
|
|
(plan-hash plan-with-channel-pin)))
|
|
(channels (json-list-ref generation "channels"))
|
|
(channel (car channels)))
|
|
(test-equal "prepared generation records channel pins"
|
|
"ready"
|
|
(json-ref prepared "status"))
|
|
(test-equal "active generation keeps channel pins"
|
|
"healthy"
|
|
(json-ref committed "status"))
|
|
(test-equal "generation channel pin records pulled commit"
|
|
"pulled456"
|
|
(json-ref channel "commit"))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper _get-builds _get-pulls _get-switches)
|
|
(prepare-plugins! state helper
|
|
(plan-plugins plan-with-branch-channel)
|
|
(plan-hash plan-with-branch-channel)
|
|
no-frame
|
|
#:plan plan-with-branch-channel)
|
|
(let* ((generation (find-generation-by-plan-hash
|
|
(state-store-read-generations state)
|
|
(plan-hash plan-with-branch-channel)))
|
|
(channels (json-list-ref generation "channels"))
|
|
(channel (car channels)))
|
|
(test-equal "branch channel records exact pulled commit"
|
|
"pulled456"
|
|
(json-ref channel "commit"))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let ((prepared (prepare-plugins! state helper
|
|
(plan-plugins plan-without-channel-delta)
|
|
(plan-hash plan-without-channel-delta)
|
|
no-frame
|
|
#:pull-required?
|
|
(plan-requires-pull?
|
|
plan-without-channel-delta))))
|
|
(test-equal "prepare skips pull for plans with no channel delta"
|
|
"ready"
|
|
(json-ref prepared "status"))
|
|
(test-equal "no-channel-delta prepare does not pull"
|
|
0 (get-pulls))
|
|
(test-equal "no-channel-delta prepare still builds"
|
|
1 (get-builds))
|
|
(test-equal "no-channel-delta prepare does not switch"
|
|
0 (get-switches))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(with-fake-rollback-herd
|
|
fixture
|
|
(lambda ()
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((prepared-a (prepare-plugins! state helper (plan-plugins plan-a)
|
|
(plan-hash plan-a) no-frame))
|
|
(_ca (commit-plan! state helper (plan-hash plan-a) no-frame))
|
|
(_pb (prepare-plugins! state helper (plan-plugins plan-b)
|
|
(plan-hash plan-b) no-frame))
|
|
(_cb (commit-plan! state helper (plan-hash plan-b) no-frame))
|
|
(rollback (rollback-store-path! state helper
|
|
(json-ref prepared-a "store_path")
|
|
#f no-frame)))
|
|
(test-equal "direct rollback succeeds for retained generation"
|
|
"healthy" (json-ref rollback "status"))
|
|
(test-equal "direct rollback does not rebuild"
|
|
2 (get-builds))
|
|
(test-equal "direct rollback performs a third switch"
|
|
3 (get-switches))
|
|
(test-equal "rolled back system points at prior store path"
|
|
(json-ref prepared-a "store_path")
|
|
(state-store-running-system-path state))))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture)))
|
|
(baseline-store-path (seed-profile-generation! fixture 1 "baseline-system"))
|
|
(system-profile-link (assq-ref fixture 'system-profile-link))
|
|
(current-system-link (assq-ref fixture 'current-system-link)))
|
|
(delete-if-present system-profile-link)
|
|
(symlink (string-append (assq-ref fixture 'profiles-directory)
|
|
"/system-1-link")
|
|
system-profile-link)
|
|
(delete-if-present current-system-link)
|
|
(symlink (string-append (assq-ref fixture 'profiles-directory)
|
|
"/system-1-link")
|
|
current-system-link)
|
|
(with-fake-rollback-herd
|
|
fixture
|
|
(lambda ()
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((_prepared (prepare-plugins! state helper (plan-plugins plan-a)
|
|
(plan-hash plan-a) no-frame))
|
|
(_commit (commit-plan! state helper (plan-hash plan-a) no-frame))
|
|
(rollback (rollback-store-path! state helper baseline-store-path
|
|
#f no-frame)))
|
|
(test-equal "rollback can switch to an unrecorded Guix profile generation"
|
|
"healthy" (json-ref rollback "status"))
|
|
(test-equal "profile-generation rollback does not rebuild"
|
|
1 (get-builds))
|
|
(test-equal "profile-generation rollback performs the second switch"
|
|
2 (get-switches))
|
|
(test-equal "profile-generation rollback returns to the baseline store path"
|
|
baseline-store-path
|
|
(state-store-running-system-path state))))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(with-fake-rollback-herd
|
|
fixture
|
|
(lambda ()
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((prepared-a (prepare-plugins! state helper (plan-plugins plan-a)
|
|
(plan-hash plan-a) no-frame))
|
|
(_ca (commit-plan! state helper (plan-hash plan-a) no-frame))
|
|
(_pb (prepare-plugins! state helper (plan-plugins plan-b)
|
|
(plan-hash plan-b) no-frame))
|
|
(_cb (commit-plan! state helper (plan-hash plan-b) no-frame))
|
|
(_drop (delete-directory-if-present
|
|
(json-ref prepared-a "store_path")))
|
|
(rollback (rollback-store-path! state helper
|
|
(json-ref prepared-a "store_path")
|
|
plan-a no-frame))
|
|
(generation-a (find-generation-by-plan-hash
|
|
(state-store-read-generations state)
|
|
(plan-hash plan-a))))
|
|
(test-equal "rollback rebuilds from plan when store path is gone"
|
|
"healthy" (json-ref rollback "status"))
|
|
(test-equal "rebuild fallback performs a third build"
|
|
3 (get-builds))
|
|
(test-equal "rebuild fallback performs a third switch"
|
|
3 (get-switches))
|
|
(test-assert "rebuild fallback records a new active generation"
|
|
(> (json-ref generation-a "generation_number") 1))
|
|
(test-assert "rebuild fallback activates a new store path"
|
|
(not (string=? (state-store-running-system-path state)
|
|
(json-ref prepared-a "store_path"))))))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture))))
|
|
(with-fake-rollback-herd
|
|
fixture
|
|
(lambda ()
|
|
(call-with-values (lambda () (make-fake-helper fixture))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((prepared-a (prepare-plugins! state helper (plan-plugins plan-a)
|
|
(plan-hash plan-a) no-frame))
|
|
(_ca (commit-plan! state helper (plan-hash plan-a) no-frame))
|
|
(_drop (delete-directory-if-present
|
|
(json-ref prepared-a "store_path")))
|
|
(rollback (rollback-store-path! state helper
|
|
(json-ref prepared-a "store_path")
|
|
#f no-frame)))
|
|
(test-equal "rollback without a retained store path or plan is infeasible"
|
|
"rollback_infeasible" (json-ref rollback "code"))))))))
|
|
|
|
(let* ((fixture (make-fixture))
|
|
(state (make-state-store (fixture->config fixture)))
|
|
(current-system-link (assq-ref fixture 'current-system-link))
|
|
(old-running (string-append (assq-ref fixture 'store-directory)
|
|
"/running-before-commit")))
|
|
(ensure-directory old-running)
|
|
(delete-if-present current-system-link)
|
|
(symlink old-running current-system-link)
|
|
(call-with-values (lambda () (make-fake-helper fixture #:diverge-running? #t))
|
|
(lambda (helper get-builds get-pulls get-switches)
|
|
(let* ((prepared (prepare-plugins! state helper (plan-plugins plan-a)
|
|
(plan-hash plan-a) no-frame))
|
|
(commit (commit-plan! state helper (plan-hash plan-a) no-frame)))
|
|
(test-equal "commit reports selected store path even if running link lags"
|
|
(json-ref prepared "store_path")
|
|
(json-ref commit "store_path"))
|
|
(test-equal "commit exposes selected system separately"
|
|
(json-ref prepared "store_path")
|
|
(json-ref commit "selectedSystem"))
|
|
(test-equal "commit preserves divergent running system for diagnostics"
|
|
old-running
|
|
(json-ref commit "runningSystem"))))))
|
|
|
|
(call-with-values (lambda () (resolve-deployment '()))
|
|
(lambda (status payload)
|
|
(test-equal "resolve-deployment wraps successful plans with 200"
|
|
200 status)
|
|
(test-assert "resolve-deployment success response is ok"
|
|
(equal? (json-ref payload "ok") #t))
|
|
(test-assert "resolve-deployment success response contains a plan"
|
|
(json-object? (json-ref payload "plan")))
|
|
(test-assert "resolve-deployment success plan has a hash"
|
|
(string? (json-ref (json-ref payload "plan") "plan_hash")))))
|
|
|
|
(call-with-values
|
|
(lambda ()
|
|
(resolve-deployment
|
|
'(("plugins" . ((("plugin_name" . "aether") ("enabled" . #t))
|
|
(("plugin_name" . "aether") ("enabled" . #t)))))))
|
|
(lambda (status payload)
|
|
(test-equal "resolve-deployment returns 409 for explicit resolver errors"
|
|
409 status)
|
|
(test-assert "resolve-deployment conflict payload is an explicit resolver error"
|
|
(and (equal? (json-ref payload "ok") #f)
|
|
(json-object? (json-ref payload "error"))))))
|
|
|
|
(test-end "tribes-deploy-operations"))
|
|
|
|
(run-tests-when-script "tests/tribes-deploy-operations.scm" run-tests)
|