@soulguard/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/cli.js +8838 -0
- package/dist/index.js +6555 -0
- package/package.json +35 -0
- package/src/approve.test.ts +386 -0
- package/src/approve.ts +352 -0
- package/src/cli/approve-command.ts +107 -0
- package/src/cli/diff-command.test.ts +61 -0
- package/src/cli/diff-command.ts +81 -0
- package/src/cli/init-command.ts +83 -0
- package/src/cli/reset-command.ts +36 -0
- package/src/cli/status-command.test.ts +90 -0
- package/src/cli/status-command.ts +78 -0
- package/src/cli/sync-command.test.ts +67 -0
- package/src/cli/sync-command.ts +105 -0
- package/src/cli.ts +224 -0
- package/src/console-live.ts +32 -0
- package/src/console-mock.ts +48 -0
- package/src/console.ts +12 -0
- package/src/constants.ts +21 -0
- package/src/diff.test.ts +189 -0
- package/src/diff.ts +212 -0
- package/src/git.test.ts +180 -0
- package/src/git.ts +131 -0
- package/src/glob.test.ts +74 -0
- package/src/glob.ts +84 -0
- package/src/index.ts +100 -0
- package/src/init.test.ts +234 -0
- package/src/init.ts +317 -0
- package/src/policy.test.ts +123 -0
- package/src/policy.ts +100 -0
- package/src/reset.test.ts +68 -0
- package/src/reset.ts +63 -0
- package/src/result.ts +14 -0
- package/src/schema.test.ts +27 -0
- package/src/schema.ts +22 -0
- package/src/self-protection.test.ts +139 -0
- package/src/self-protection.ts +63 -0
- package/src/status.test.ts +241 -0
- package/src/status.ts +114 -0
- package/src/sync.test.ts +172 -0
- package/src/sync.ts +101 -0
- package/src/system-ops-mock.ts +243 -0
- package/src/system-ops-node.test.ts +183 -0
- package/src/system-ops-node.ts +499 -0
- package/src/system-ops.ts +109 -0
- package/src/types.ts +91 -0
- package/src/vault-check.test.ts +41 -0
- package/src/vault-check.ts +24 -0
- package/test-e2e/Dockerfile +29 -0
- package/test-e2e/cases/approve-with-hash/expected.txt +16 -0
- package/test-e2e/cases/approve-with-hash/test.sh +23 -0
- package/test-e2e/cases/diff-no-changes/expected.txt +5 -0
- package/test-e2e/cases/diff-no-changes/test.sh +7 -0
- package/test-e2e/cases/diff-no-staging/expected.txt +1 -0
- package/test-e2e/cases/diff-no-staging/test.sh +6 -0
- package/test-e2e/cases/diff-shows-changes/expected.txt +13 -0
- package/test-e2e/cases/diff-shows-changes/test.sh +12 -0
- package/test-e2e/cases/diff-vault-missing/expected.txt +6 -0
- package/test-e2e/cases/diff-vault-missing/test.sh +10 -0
- package/test-e2e/cases/glob-ledger-files/expected.txt +52 -0
- package/test-e2e/cases/glob-ledger-files/test.sh +28 -0
- package/test-e2e/cases/glob-vault-files/expected.txt +41 -0
- package/test-e2e/cases/glob-vault-files/test.sh +30 -0
- package/test-e2e/cases/init-blocked-by-agent/expected.txt +11 -0
- package/test-e2e/cases/init-blocked-by-agent/test.sh +15 -0
- package/test-e2e/cases/init-happy/expected.txt +18 -0
- package/test-e2e/cases/init-happy/test.sh +20 -0
- package/test-e2e/cases/init-idempotent/expected.txt +13 -0
- package/test-e2e/cases/init-idempotent/test.sh +14 -0
- package/test-e2e/cases/reset-staging/expected.txt +16 -0
- package/test-e2e/cases/reset-staging/test.sh +23 -0
- package/test-e2e/cases/self-protection-blocks-invalid/expected.txt +12 -0
- package/test-e2e/cases/self-protection-blocks-invalid/test.sh +25 -0
- package/test-e2e/cases/status-clean/expected.txt +9 -0
- package/test-e2e/cases/status-clean/test.sh +8 -0
- package/test-e2e/cases/status-drifted/expected.txt +9 -0
- package/test-e2e/cases/status-drifted/test.sh +11 -0
- package/test-e2e/cases/status-no-config/expected.txt +1 -0
- package/test-e2e/cases/status-no-config/test.sh +2 -0
- package/test-e2e/cases/sync-fix-drift/expected.txt +15 -0
- package/test-e2e/cases/sync-fix-drift/test.sh +14 -0
- package/test-e2e/cases/sync-no-sudo-clean/expected.txt +11 -0
- package/test-e2e/cases/sync-no-sudo-clean/test.sh +9 -0
- package/test-e2e/cases/sync-no-sudo-drift/expected.txt +7 -0
- package/test-e2e/cases/sync-no-sudo-drift/test.sh +10 -0
- package/test-e2e/cases/vault-remove-file/expected.txt +26 -0
- package/test-e2e/cases/vault-remove-file/test.sh +31 -0
- package/test-e2e/cases/vault-write-blocked/expected.txt +8 -0
- package/test-e2e/cases/vault-write-blocked/test.sh +14 -0
- package/test-e2e/run-tests.sh +76 -0
- package/test-integration/Dockerfile +17 -0
- package/test-integration/system-ops-node.integration.test.ts +170 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Agent can't fix drift without sudo (no init = no scoped sudoers)
|
|
2
|
+
echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
|
|
3
|
+
echo '# My Soul' > SOUL.md
|
|
4
|
+
|
|
5
|
+
# Make workspace readable by agent (no init, no sudoers)
|
|
6
|
+
chmod 755 .
|
|
7
|
+
chmod o+r soulguard.json SOUL.md
|
|
8
|
+
|
|
9
|
+
# Agent syncs drifted workspace without sudo — chown should fail
|
|
10
|
+
su - agent -c "soulguard sync $(pwd)"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Soulguard Init — /workspace
|
|
2
|
+
Created group: soulguard
|
|
3
|
+
Created user: soulguardian
|
|
4
|
+
Wrote /etc/sudoers.d/soulguard
|
|
5
|
+
Created .soulguard/staging/
|
|
6
|
+
Synced 3 vault file(s)
|
|
7
|
+
|
|
8
|
+
Done.
|
|
9
|
+
DIFF:
|
|
10
|
+
Soulguard Diff — /workspace
|
|
11
|
+
|
|
12
|
+
🗑️ BOOTSTRAP.md (staged for deletion)
|
|
13
|
+
✅ SOUL.md (no changes)
|
|
14
|
+
✅ soulguard.json (no changes)
|
|
15
|
+
|
|
16
|
+
1 file(s) changed
|
|
17
|
+
Approval hash: 24d6aaec960a3d74c84690b786336bdcce7c35585e353be867f6f0606093618e
|
|
18
|
+
|
|
19
|
+
Approved 1 file(s):
|
|
20
|
+
✅ BOOTSTRAP.md
|
|
21
|
+
|
|
22
|
+
Vault updated. Staging synced.
|
|
23
|
+
BOOTSTRAP EXISTS:
|
|
24
|
+
no
|
|
25
|
+
SOUL EXISTS:
|
|
26
|
+
yes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Vault file deletion: agent deletes a file from staging, owner approves.
|
|
2
|
+
# Tests the full lifecycle when a vault-protected file is deleted through staging.
|
|
3
|
+
|
|
4
|
+
# Setup: two vault files
|
|
5
|
+
cat > soulguard.json <<'EOF'
|
|
6
|
+
{"vault":["SOUL.md","BOOTSTRAP.md","soulguard.json"],"ledger":[]}
|
|
7
|
+
EOF
|
|
8
|
+
echo '# My Soul' > SOUL.md
|
|
9
|
+
echo '# Bootstrap' > BOOTSTRAP.md
|
|
10
|
+
|
|
11
|
+
# Owner runs init
|
|
12
|
+
soulguard init . --agent-user agent
|
|
13
|
+
|
|
14
|
+
# Agent deletes BOOTSTRAP.md from staging (done with it)
|
|
15
|
+
su - agent -c "rm $(pwd)/.soulguard/staging/BOOTSTRAP.md"
|
|
16
|
+
|
|
17
|
+
# Diff should show deletion
|
|
18
|
+
echo "DIFF:"
|
|
19
|
+
NO_COLOR=1 soulguard diff . 2>&1
|
|
20
|
+
|
|
21
|
+
# Get hash and approve the deletion
|
|
22
|
+
HASH=$(NO_COLOR=1 soulguard diff . 2>&1 | grep 'Approval hash:' | awk '{print $NF}')
|
|
23
|
+
soulguard approve . --hash "$HASH"
|
|
24
|
+
|
|
25
|
+
# BOOTSTRAP.md should be gone from disk
|
|
26
|
+
echo "BOOTSTRAP EXISTS:"
|
|
27
|
+
test -f BOOTSTRAP.md && echo "yes" || echo "no"
|
|
28
|
+
|
|
29
|
+
# SOUL.md should still exist
|
|
30
|
+
echo "SOUL EXISTS:"
|
|
31
|
+
test -f SOUL.md && echo "yes" || echo "no"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Setup: create and protect a vault file (as owner/root)
|
|
2
|
+
echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
|
|
3
|
+
echo '# My Soul' > SOUL.md
|
|
4
|
+
soulguard init . --agent-user agent > /dev/null 2>&1
|
|
5
|
+
soulguard sync .
|
|
6
|
+
|
|
7
|
+
# Attack 1: Agent tries to write to the vaulted file (should fail)
|
|
8
|
+
su - agent -c "(echo hacked > $(pwd)/SOUL.md) 2>&1" && echo "WRITE SUCCEEDED (BAD)" || echo "WRITE BLOCKED (GOOD)"
|
|
9
|
+
|
|
10
|
+
# Attack 2: Agent tries to chown the file back (should fail without root)
|
|
11
|
+
su - agent -c "chown agent:agent $(pwd)/SOUL.md 2>&1" && echo "CHOWN SUCCEEDED (BAD)" || echo "CHOWN BLOCKED (GOOD)"
|
|
12
|
+
|
|
13
|
+
# Verify file is still intact
|
|
14
|
+
cat SOUL.md
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# E2E snapshot test runner for soulguard CLI.
|
|
3
|
+
#
|
|
4
|
+
# Runs each test case in a separate Docker container for full isolation.
|
|
5
|
+
# No shared state between tests — each gets a fresh filesystem.
|
|
6
|
+
#
|
|
7
|
+
# Each test case is a directory under cases/ containing:
|
|
8
|
+
# test.sh — shell script to run (executed as `agent` user)
|
|
9
|
+
# expected.txt — exact expected stdout+stderr output
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ./run-tests.sh # run all tests, diff against snapshots
|
|
13
|
+
# ./run-tests.sh --update # regenerate all snapshots
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
CASES_DIR="$SCRIPT_DIR/cases"
|
|
19
|
+
IMAGE="soulguard-e2e"
|
|
20
|
+
UPDATE="${1:-}"
|
|
21
|
+
PASS=0
|
|
22
|
+
FAIL=0
|
|
23
|
+
|
|
24
|
+
for case_dir in "$CASES_DIR"/*/; do
|
|
25
|
+
test_name="$(basename "$case_dir")"
|
|
26
|
+
test_script="$case_dir/test.sh"
|
|
27
|
+
expected_file="$case_dir/expected.txt"
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$test_script" ]; then
|
|
30
|
+
echo "SKIP: $test_name (no test.sh)"
|
|
31
|
+
continue
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Run test in a fresh container — complete isolation
|
|
35
|
+
actual=$(docker run --rm "$IMAGE" bash -c "
|
|
36
|
+
workspace=\$(mktemp -d /tmp/soulguard-e2e-XXXX)
|
|
37
|
+
chmod 755 \"\$workspace\"
|
|
38
|
+
cd \"\$workspace\"
|
|
39
|
+
NO_COLOR=1 bash /app/packages/core/test-e2e/cases/$test_name/test.sh 2>&1 | \
|
|
40
|
+
sed \"s|\$workspace|/workspace|g\" | \
|
|
41
|
+
sed 's|/app/packages/core/test-e2e/cases/$test_name/test.sh|test.sh|g'
|
|
42
|
+
") || true
|
|
43
|
+
|
|
44
|
+
if [ "$UPDATE" = "--update" ]; then
|
|
45
|
+
echo "$actual" > "$expected_file"
|
|
46
|
+
echo "UPDATED: $test_name"
|
|
47
|
+
continue
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ ! -f "$expected_file" ]; then
|
|
51
|
+
echo "FAIL: $test_name (no expected.txt — run with --update)"
|
|
52
|
+
echo "--- actual output ---"
|
|
53
|
+
echo "$actual"
|
|
54
|
+
echo "---"
|
|
55
|
+
FAIL=$((FAIL + 1))
|
|
56
|
+
continue
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
expected=$(cat "$expected_file")
|
|
60
|
+
|
|
61
|
+
if [ "$actual" = "$expected" ]; then
|
|
62
|
+
echo "PASS: $test_name"
|
|
63
|
+
PASS=$((PASS + 1))
|
|
64
|
+
else
|
|
65
|
+
echo "FAIL: $test_name"
|
|
66
|
+
diff --color=auto <(echo "$expected") <(echo "$actual") || true
|
|
67
|
+
FAIL=$((FAIL + 1))
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
echo "$PASS passed, $FAIL failed"
|
|
73
|
+
|
|
74
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM oven/bun:latest
|
|
2
|
+
|
|
3
|
+
# Create test users that mirror the soulguard design
|
|
4
|
+
RUN groupadd soulguard && \
|
|
5
|
+
useradd -r -g soulguard soulguardian && \
|
|
6
|
+
useradd -g staff agent 2>/dev/null || useradd agent
|
|
7
|
+
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
|
|
10
|
+
# Copy full monorepo (respects .dockerignore)
|
|
11
|
+
COPY . .
|
|
12
|
+
|
|
13
|
+
# Install deps
|
|
14
|
+
RUN bun install --frozen-lockfile
|
|
15
|
+
|
|
16
|
+
# Run integration tests as root (needed for chown)
|
|
17
|
+
CMD ["bun", "test", "packages/core/test-integration/"]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for NodeSystemOps — requires root + test users.
|
|
3
|
+
*
|
|
4
|
+
* Run via Docker:
|
|
5
|
+
* docker build -f packages/core/test-integration/Dockerfile -t soulguard-test .
|
|
6
|
+
* docker run --rm soulguard-test
|
|
7
|
+
*
|
|
8
|
+
* These tests exercise chown (which requires root) and verify the full
|
|
9
|
+
* ownership pipeline: create file → chown to soulguardian → verify stat.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
13
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { NodeSystemOps } from "../src/system-ops-node.js";
|
|
17
|
+
|
|
18
|
+
let workspace: string;
|
|
19
|
+
let ops: NodeSystemOps;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
workspace = await mkdtemp(join(tmpdir(), "soulguard-integ-"));
|
|
23
|
+
ops = new NodeSystemOps(workspace);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await rm(workspace, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("NodeSystemOps integration (root + test users)", () => {
|
|
31
|
+
// ── chown ───────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("chown", () => {
|
|
34
|
+
test("transfers file to soulguardian:soulguard", async () => {
|
|
35
|
+
await writeFile(join(workspace, "SOUL.md"), "# Soul");
|
|
36
|
+
|
|
37
|
+
const result = await ops.chown("SOUL.md", {
|
|
38
|
+
user: "soulguardian",
|
|
39
|
+
group: "soulguard",
|
|
40
|
+
});
|
|
41
|
+
expect(result.ok).toBe(true);
|
|
42
|
+
|
|
43
|
+
const stat = await ops.stat("SOUL.md");
|
|
44
|
+
expect(stat.ok).toBe(true);
|
|
45
|
+
if (!stat.ok) return;
|
|
46
|
+
expect(stat.value.ownership.user).toBe("soulguardian");
|
|
47
|
+
expect(stat.value.ownership.group).toBe("soulguard");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("transfers file back to agent user", async () => {
|
|
51
|
+
await writeFile(join(workspace, "notes.md"), "# Notes");
|
|
52
|
+
|
|
53
|
+
// First transfer to soulguardian
|
|
54
|
+
await ops.chown("notes.md", {
|
|
55
|
+
user: "soulguardian",
|
|
56
|
+
group: "soulguard",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Then release back to agent
|
|
60
|
+
const result = await ops.chown("notes.md", {
|
|
61
|
+
user: "agent",
|
|
62
|
+
group: "soulguard",
|
|
63
|
+
});
|
|
64
|
+
expect(result.ok).toBe(true);
|
|
65
|
+
|
|
66
|
+
const stat = await ops.stat("notes.md");
|
|
67
|
+
expect(stat.ok).toBe(true);
|
|
68
|
+
if (!stat.ok) return;
|
|
69
|
+
expect(stat.value.ownership.user).toBe("agent");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns error for nonexistent file", async () => {
|
|
73
|
+
const result = await ops.chown("nope.md", {
|
|
74
|
+
user: "soulguardian",
|
|
75
|
+
group: "soulguard",
|
|
76
|
+
});
|
|
77
|
+
expect(result.ok).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("returns error for nonexistent user", async () => {
|
|
81
|
+
await writeFile(join(workspace, "test.md"), "hello");
|
|
82
|
+
|
|
83
|
+
const result = await ops.chown("test.md", {
|
|
84
|
+
user: "nonexistent_user_xyz",
|
|
85
|
+
group: "soulguard",
|
|
86
|
+
});
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── Full vault workflow ─────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
describe("vault workflow", () => {
|
|
94
|
+
test("protect → verify → release cycle", async () => {
|
|
95
|
+
await writeFile(join(workspace, "SOUL.md"), "# My Soul");
|
|
96
|
+
|
|
97
|
+
// 1. Protect: chown + chmod
|
|
98
|
+
const chownResult = await ops.chown("SOUL.md", {
|
|
99
|
+
user: "soulguardian",
|
|
100
|
+
group: "soulguard",
|
|
101
|
+
});
|
|
102
|
+
expect(chownResult.ok).toBe(true);
|
|
103
|
+
|
|
104
|
+
const chmodResult = await ops.chmod("SOUL.md", "444");
|
|
105
|
+
expect(chmodResult.ok).toBe(true);
|
|
106
|
+
|
|
107
|
+
// 2. Verify: stat shows correct ownership + mode
|
|
108
|
+
const stat1 = await ops.stat("SOUL.md");
|
|
109
|
+
expect(stat1.ok).toBe(true);
|
|
110
|
+
if (!stat1.ok) return;
|
|
111
|
+
expect(stat1.value.ownership.user).toBe("soulguardian");
|
|
112
|
+
expect(stat1.value.ownership.group).toBe("soulguard");
|
|
113
|
+
expect(stat1.value.ownership.mode).toBe("444");
|
|
114
|
+
|
|
115
|
+
// 3. Content still readable
|
|
116
|
+
const content = await ops.readFile("SOUL.md");
|
|
117
|
+
expect(content.ok).toBe(true);
|
|
118
|
+
if (!content.ok) return;
|
|
119
|
+
expect(content.value).toBe("# My Soul");
|
|
120
|
+
|
|
121
|
+
// 4. Hash still works
|
|
122
|
+
const hash = await ops.hashFile("SOUL.md");
|
|
123
|
+
expect(hash.ok).toBe(true);
|
|
124
|
+
if (!hash.ok) return;
|
|
125
|
+
expect(hash.value).toMatch(/^[a-f0-9]{64}$/);
|
|
126
|
+
|
|
127
|
+
// 5. Release: chown back + chmod
|
|
128
|
+
const releaseChown = await ops.chown("SOUL.md", {
|
|
129
|
+
user: "agent",
|
|
130
|
+
group: "soulguard",
|
|
131
|
+
});
|
|
132
|
+
expect(releaseChown.ok).toBe(true);
|
|
133
|
+
|
|
134
|
+
const releaseChmod = await ops.chmod("SOUL.md", "644");
|
|
135
|
+
expect(releaseChmod.ok).toBe(true);
|
|
136
|
+
|
|
137
|
+
// 6. Verify released state
|
|
138
|
+
const stat2 = await ops.stat("SOUL.md");
|
|
139
|
+
expect(stat2.ok).toBe(true);
|
|
140
|
+
if (!stat2.ok) return;
|
|
141
|
+
expect(stat2.value.ownership.user).toBe("agent");
|
|
142
|
+
expect(stat2.value.ownership.mode).toBe("644");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── userExists / groupExists with real users ────────────────────────
|
|
147
|
+
|
|
148
|
+
describe("user/group checks with test users", () => {
|
|
149
|
+
test("soulguardian user exists", async () => {
|
|
150
|
+
const result = await ops.userExists("soulguardian");
|
|
151
|
+
expect(result.ok).toBe(true);
|
|
152
|
+
if (!result.ok) return;
|
|
153
|
+
expect(result.value).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("soulguard group exists", async () => {
|
|
157
|
+
const result = await ops.groupExists("soulguard");
|
|
158
|
+
expect(result.ok).toBe(true);
|
|
159
|
+
if (!result.ok) return;
|
|
160
|
+
expect(result.value).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("agent user exists", async () => {
|
|
164
|
+
const result = await ops.userExists("agent");
|
|
165
|
+
expect(result.ok).toBe(true);
|
|
166
|
+
if (!result.ok) return;
|
|
167
|
+
expect(result.value).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|