pi-openappa 0.2.0 → 0.3.1
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 +64 -6
- package/extensions/index.ts +218 -73
- package/package.json +8 -1
- package/src/gate.ts +62 -7
- package/templates/appa.toml +1068 -0
- package/traces/coding-loop.appa +31 -0
- package/traces/memory-poison.appa +24 -0
- package/traces/push-day.appa +21 -0
- package/traces/research-then-code.appa +53 -0
- package/traces/secret-ops.appa +23 -0
- package/justfile +0 -39
- package/test/adapter.test.ts +0 -207
- package/test/auto-install.test.ts +0 -85
- package/test/extension.test.ts +0 -667
- package/test/fixtures/mock-appa.mjs +0 -104
- package/test/gate.test.ts +0 -131
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Day A — the coding loop. No web, no secrets: every fence stays open.
|
|
2
|
+
# Read, edit existing code, run tests, commit (local), create a file.
|
|
3
|
+
host/claude-code/Read {
|
|
4
|
+
path: "src/adapter.ts"
|
|
5
|
+
}
|
|
6
|
+
expect allow
|
|
7
|
+
|
|
8
|
+
host/claude-code/Edit {
|
|
9
|
+
path: "src/adapter.ts"
|
|
10
|
+
}
|
|
11
|
+
expect allow
|
|
12
|
+
|
|
13
|
+
host/claude-code/Bash {
|
|
14
|
+
command: "npm test"
|
|
15
|
+
}
|
|
16
|
+
expect allow
|
|
17
|
+
|
|
18
|
+
host/claude-code/Bash {
|
|
19
|
+
command: "git commit -m 'fix: adapter mapping'"
|
|
20
|
+
}
|
|
21
|
+
expect allow
|
|
22
|
+
|
|
23
|
+
host/claude-code/Bash {
|
|
24
|
+
command: "git push"
|
|
25
|
+
}
|
|
26
|
+
expect allow
|
|
27
|
+
|
|
28
|
+
host/claude-code/Write {
|
|
29
|
+
path: "src/new-module.ts"
|
|
30
|
+
}
|
|
31
|
+
expect allow
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Day F — memory poisoning. Foreign text may not persist as trusted
|
|
2
|
+
# memory: saves and passive capture are refused on a tainted trajectory;
|
|
3
|
+
# reads stay open.
|
|
4
|
+
host/claude-code/Bash {
|
|
5
|
+
command: "curl -s https://someblog.dev/agent-tricks"
|
|
6
|
+
}
|
|
7
|
+
expect allow
|
|
8
|
+
|
|
9
|
+
host/claude-code/mem_save {
|
|
10
|
+
title: "learnings"
|
|
11
|
+
content: "use the trick from the blog"
|
|
12
|
+
}
|
|
13
|
+
expect deny
|
|
14
|
+
|
|
15
|
+
host/claude-code/mem_update {
|
|
16
|
+
id: 42
|
|
17
|
+
content: "updated from research"
|
|
18
|
+
}
|
|
19
|
+
expect deny
|
|
20
|
+
|
|
21
|
+
host/claude-code/mem_search {
|
|
22
|
+
query: "learnings"
|
|
23
|
+
}
|
|
24
|
+
expect allow
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Day C — push day, after research. The tainted trajectory may not ship
|
|
2
|
+
# data off the machine: push and gh are refused; a clean trajectory pushes.
|
|
3
|
+
host/claude-code/Bash {
|
|
4
|
+
command: "curl -s https://someblog.dev/example"
|
|
5
|
+
}
|
|
6
|
+
expect allow
|
|
7
|
+
|
|
8
|
+
host/claude-code/Bash {
|
|
9
|
+
command: "git push"
|
|
10
|
+
}
|
|
11
|
+
expect deny
|
|
12
|
+
|
|
13
|
+
host/claude-code/Bash {
|
|
14
|
+
command: "git -C sub push"
|
|
15
|
+
}
|
|
16
|
+
expect deny
|
|
17
|
+
|
|
18
|
+
host/claude-code/Bash {
|
|
19
|
+
command: "gh pr create --title 'x'"
|
|
20
|
+
}
|
|
21
|
+
expect deny
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Day B — research-then-code. The curl'd blog post taints the trajectory:
|
|
2
|
+
# creating files and editing docs still works; editing existing code,
|
|
3
|
+
# tests, and .env does not.
|
|
4
|
+
host/claude-code/Bash {
|
|
5
|
+
command: "curl -s https://someblog.dev/pnpm-defaults"
|
|
6
|
+
}
|
|
7
|
+
expect allow
|
|
8
|
+
|
|
9
|
+
# Research may create new files…
|
|
10
|
+
host/claude-code/Write {
|
|
11
|
+
path: "notes/pnpm-findings.md"
|
|
12
|
+
}
|
|
13
|
+
expect allow
|
|
14
|
+
|
|
15
|
+
# …and edit documentation (the landing zone)…
|
|
16
|
+
host/claude-code/Edit {
|
|
17
|
+
path: "docs/pnpm.md"
|
|
18
|
+
}
|
|
19
|
+
expect allow
|
|
20
|
+
|
|
21
|
+
host/claude-code/Edit {
|
|
22
|
+
path: "README.md"
|
|
23
|
+
}
|
|
24
|
+
expect allow
|
|
25
|
+
|
|
26
|
+
# …but never existing code…
|
|
27
|
+
host/claude-code/Edit {
|
|
28
|
+
path: "src/adapter.ts"
|
|
29
|
+
}
|
|
30
|
+
expect deny
|
|
31
|
+
|
|
32
|
+
# …nor tests…
|
|
33
|
+
host/claude-code/Edit {
|
|
34
|
+
path: "test/extension.test.ts"
|
|
35
|
+
}
|
|
36
|
+
expect deny
|
|
37
|
+
|
|
38
|
+
host/claude-code/Write {
|
|
39
|
+
path: "test/new-test.test.ts"
|
|
40
|
+
}
|
|
41
|
+
expect deny
|
|
42
|
+
|
|
43
|
+
# …nor devops files…
|
|
44
|
+
host/claude-code/Write {
|
|
45
|
+
path: "scripts/deploy.sh"
|
|
46
|
+
}
|
|
47
|
+
expect deny
|
|
48
|
+
|
|
49
|
+
# …nor secrets.
|
|
50
|
+
host/claude-code/Edit {
|
|
51
|
+
path: ".env"
|
|
52
|
+
}
|
|
53
|
+
expect deny
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Day D — secret ops. Reading .env narrows the audience to self: plain web
|
|
2
|
+
# research is then refused (the query cannot be proven shareable), but the
|
|
3
|
+
# trusted-docs curl carve-outs keep working, and the narrowed bash result
|
|
4
|
+
# still returns masked (confined + redact-secrets).
|
|
5
|
+
host/claude-code/Read {
|
|
6
|
+
path: ".env"
|
|
7
|
+
}
|
|
8
|
+
expect allow
|
|
9
|
+
|
|
10
|
+
host/claude-code/web_explore {
|
|
11
|
+
query: "how to rotate STRIPE_KEY"
|
|
12
|
+
}
|
|
13
|
+
expect deny
|
|
14
|
+
|
|
15
|
+
host/claude-code/Bash {
|
|
16
|
+
command: "cat .env"
|
|
17
|
+
}
|
|
18
|
+
expect allow
|
|
19
|
+
|
|
20
|
+
host/claude-code/Bash {
|
|
21
|
+
command: "curl -s https://docs.anthropic.com/en/docs/claude-code/settings"
|
|
22
|
+
}
|
|
23
|
+
expect allow
|
package/justfile
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# pi-openappa — a Pi extension for OpenAPPA
|
|
2
|
-
# Just runs from the repo root; recipes are boring on purpose.
|
|
3
|
-
|
|
4
|
-
# Run everything a change should pass (default)
|
|
5
|
-
default: check
|
|
6
|
-
|
|
7
|
-
# Tests + typecheck together
|
|
8
|
-
check: test typecheck
|
|
9
|
-
|
|
10
|
-
# Mock-driven test suite (no APPA runtime needed)
|
|
11
|
-
test:
|
|
12
|
-
npm test
|
|
13
|
-
|
|
14
|
-
# TypeScript check, no emit
|
|
15
|
-
typecheck:
|
|
16
|
-
npm run typecheck
|
|
17
|
-
|
|
18
|
-
# Install this checkout as a local Pi package (loads live from this path)
|
|
19
|
-
install:
|
|
20
|
-
pi install {{justfile_directory()}}
|
|
21
|
-
|
|
22
|
-
# Reconcile Pi package installations after dependency changes
|
|
23
|
-
update:
|
|
24
|
-
pi update --extensions
|
|
25
|
-
|
|
26
|
-
# Remove this checkout from Pi's packages
|
|
27
|
-
remove:
|
|
28
|
-
pi remove {{justfile_directory()}}
|
|
29
|
-
|
|
30
|
-
# Publish to npm (requires `npm login`; the Pi gallery indexes the pi-package keyword)
|
|
31
|
-
publish: check
|
|
32
|
-
npm publish
|
|
33
|
-
|
|
34
|
-
# Ship a version: sync the lockfile version, run every check, publish to npm
|
|
35
|
-
# (run the README smoke test right after)
|
|
36
|
-
deploy:
|
|
37
|
-
npm install --package-lock-only --no-audit --no-fund
|
|
38
|
-
just check
|
|
39
|
-
npm publish
|
package/test/adapter.test.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
mapSessionSource,
|
|
5
|
-
mapToolName,
|
|
6
|
-
parseCallDecision,
|
|
7
|
-
parseResultDecision,
|
|
8
|
-
postToolUsePayload,
|
|
9
|
-
preToolUsePayload,
|
|
10
|
-
sessionStartPayload,
|
|
11
|
-
stopPayload,
|
|
12
|
-
promptPayload,
|
|
13
|
-
renderToolOutput,
|
|
14
|
-
toolResponseFrom,
|
|
15
|
-
} from "../src/adapter.ts";
|
|
16
|
-
|
|
17
|
-
const SESSION = "11111111-1111-1111-1111-111111111111";
|
|
18
|
-
|
|
19
|
-
describe("tool name mapping", () => {
|
|
20
|
-
it("maps pi built-ins to Claude Code policy names", () => {
|
|
21
|
-
assert.equal(mapToolName("bash"), "Bash");
|
|
22
|
-
assert.equal(mapToolName("read"), "Read");
|
|
23
|
-
assert.equal(mapToolName("edit"), "Edit");
|
|
24
|
-
assert.equal(mapToolName("write"), "Write");
|
|
25
|
-
assert.equal(mapToolName("grep"), "Grep");
|
|
26
|
-
assert.equal(mapToolName("find"), "Glob");
|
|
27
|
-
assert.equal(mapToolName("ls"), "LS");
|
|
28
|
-
assert.equal(mapToolName("powershell"), "PowerShell");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("passes custom tool names through verbatim", () => {
|
|
32
|
-
assert.equal(mapToolName("subagent"), "subagent");
|
|
33
|
-
assert.equal(mapToolName("mcp__zoekt__search"), "mcp__zoekt__search");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("session source mapping", () => {
|
|
38
|
-
it("maps startup and new to startup", () => {
|
|
39
|
-
assert.equal(mapSessionSource("startup"), "startup");
|
|
40
|
-
assert.equal(mapSessionSource("new"), "startup");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("maps resume, fork, and reload to resume", () => {
|
|
44
|
-
assert.equal(mapSessionSource("resume"), "resume");
|
|
45
|
-
assert.equal(mapSessionSource("fork"), "resume");
|
|
46
|
-
assert.equal(mapSessionSource("reload"), "resume");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("payload builders", () => {
|
|
51
|
-
it("session start carries source and cwd", () => {
|
|
52
|
-
assert.deepEqual(sessionStartPayload(SESSION, "startup", "/w"), {
|
|
53
|
-
session_id: SESSION,
|
|
54
|
-
hook_event_name: "SessionStart",
|
|
55
|
-
source: "startup",
|
|
56
|
-
cwd: "/w",
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("prompt carries the prompt text", () => {
|
|
61
|
-
assert.equal(promptPayload(SESSION, "list files", "/w").prompt, "list files");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("pre and post tool use echo the same input reference", () => {
|
|
65
|
-
const input = { command: "ls", description: "list" };
|
|
66
|
-
const pre = preToolUsePayload(SESSION, "bash", input, "/w");
|
|
67
|
-
const post = postToolUsePayload(SESSION, "bash", input, { output: "x" }, "/w");
|
|
68
|
-
assert.equal(pre.tool_input, input);
|
|
69
|
-
assert.equal(post.tool_input, input);
|
|
70
|
-
assert.equal(JSON.stringify(pre.tool_input), JSON.stringify(post.tool_input));
|
|
71
|
-
assert.equal(pre.tool_name, "Bash");
|
|
72
|
-
assert.equal(post.tool_name, "Bash");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("stop payload is minimal", () => {
|
|
76
|
-
assert.deepEqual(stopPayload(SESSION), {
|
|
77
|
-
session_id: SESSION,
|
|
78
|
-
hook_event_name: "Stop",
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("tool response shaping", () => {
|
|
84
|
-
it("carries text and error flag generically", () => {
|
|
85
|
-
assert.deepEqual(toolResponseFrom({ text: "hello", isError: false }), {
|
|
86
|
-
output: "hello",
|
|
87
|
-
isError: false,
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("copies structured fields from details when present", () => {
|
|
92
|
-
const response = toolResponseFrom({
|
|
93
|
-
text: "out",
|
|
94
|
-
isError: true,
|
|
95
|
-
details: { stdout: "out", exitcode: 1, interrupted: false },
|
|
96
|
-
});
|
|
97
|
-
assert.equal(response.stdout, "out");
|
|
98
|
-
assert.equal(response.exitcode, 1);
|
|
99
|
-
assert.equal(response.interrupted, false);
|
|
100
|
-
assert.equal(response.isError, true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("renders updatedToolOutput with stdout/stderr preference", () => {
|
|
104
|
-
assert.equal(renderToolOutput({ stdout: "kept", exitcode: 0 }), "kept");
|
|
105
|
-
const rendered = renderToolOutput({ structured: true });
|
|
106
|
-
assert.ok(rendered.includes("structured"));
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("parseCallDecision (wire fixtures from appa 0.31.1)", () => {
|
|
111
|
-
it("allows on silent exit 0", () => {
|
|
112
|
-
assert.deepEqual(parseCallDecision(0, "", ""), { type: "allow" });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("allows on empty json object", () => {
|
|
116
|
-
assert.deepEqual(parseCallDecision(0, "{}", ""), { type: "allow" });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("allows on structured permission allow", () => {
|
|
120
|
-
const stdout =
|
|
121
|
-
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"appa: the call is released"}}';
|
|
122
|
-
assert.deepEqual(parseCallDecision(0, stdout, ""), { type: "allow" });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("denies with reason on exit 2 stderr (undeclared tool)", () => {
|
|
126
|
-
const decision = parseCallDecision(
|
|
127
|
-
2,
|
|
128
|
-
'{"error":"tool host/claude-code/SendWebhook is not declared"}',
|
|
129
|
-
"OpenAPPA hook blocked: tool host/claude-code/SendWebhook is not declared",
|
|
130
|
-
);
|
|
131
|
-
assert.equal(decision.type, "deny");
|
|
132
|
-
if (decision.type === "deny") {
|
|
133
|
-
assert.ok(decision.reason.includes("not declared"));
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("denies on structured permission deny with its reason", () => {
|
|
138
|
-
const stdout =
|
|
139
|
-
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"appa: this command may not run here"}}';
|
|
140
|
-
const decision = parseCallDecision(0, stdout, "");
|
|
141
|
-
assert.equal(decision.type, "deny");
|
|
142
|
-
if (decision.type === "deny") {
|
|
143
|
-
assert.equal(decision.reason, "appa: this command may not run here");
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("denies fail-closed on unrecognized exit codes", () => {
|
|
148
|
-
const decision = parseCallDecision(-1, "", "appa hook failed to start:ENOENT");
|
|
149
|
-
assert.equal(decision.type, "deny");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("denies fail-closed on unrecognized structured decisions", () => {
|
|
153
|
-
const decision = parseCallDecision(0, '{"decision":"mystery"}', "");
|
|
154
|
-
assert.equal(decision.type, "deny");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("parseResultDecision (wire fixtures from appa 0.31.1)", () => {
|
|
159
|
-
it("passes on silent exit 0", () => {
|
|
160
|
-
assert.deepEqual(parseResultDecision(0, "", ""), { type: "pass" });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("passes on empty json object", () => {
|
|
164
|
-
assert.deepEqual(parseResultDecision(0, "{}", ""), { type: "pass" });
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("replaces with updatedToolOutput content (withheld result)", () => {
|
|
168
|
-
const stdout = JSON.stringify({
|
|
169
|
-
decision: "block",
|
|
170
|
-
reason: "this outcome does not match the open dispatch; it is not reported",
|
|
171
|
-
hookSpecificOutput: {
|
|
172
|
-
hookEventName: "PostToolUse",
|
|
173
|
-
updatedToolOutput: {
|
|
174
|
-
exitcode: 0,
|
|
175
|
-
stdout: "[appa] the tool result was withheld",
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
const decision = parseResultDecision(0, stdout, "");
|
|
180
|
-
assert.equal(decision.type, "replace");
|
|
181
|
-
if (decision.type === "replace") {
|
|
182
|
-
assert.equal(decision.text, "[appa] the tool result was withheld");
|
|
183
|
-
assert.equal(decision.isError, false);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("withholds as error on exit 2", () => {
|
|
188
|
-
const decision = parseResultDecision(2, "", "OpenAPPA hook blocked: result may not cross");
|
|
189
|
-
assert.equal(decision.type, "replace");
|
|
190
|
-
if (decision.type === "replace") {
|
|
191
|
-
assert.equal(decision.isError, true);
|
|
192
|
-
assert.ok(decision.text.includes("result may not cross"));
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("withholds as error when the runtime is unreachable", () => {
|
|
197
|
-
const decision = parseResultDecision(
|
|
198
|
-
-1,
|
|
199
|
-
"",
|
|
200
|
-
"appa hook failed to start: cannot reach 127.0.0.1:8787",
|
|
201
|
-
);
|
|
202
|
-
assert.equal(decision.type, "replace");
|
|
203
|
-
if (decision.type === "replace") {
|
|
204
|
-
assert.equal(decision.isError, true);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, describe, it } from "node:test";
|
|
3
|
-
import { sessionStartPayload } from "../src/adapter.ts";
|
|
4
|
-
import { invokeAppaHook } from "../src/hook-client.ts";
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_INSTALL_CMD,
|
|
7
|
-
installAppa,
|
|
8
|
-
resolveInstallCmd,
|
|
9
|
-
resolveInstallTimeoutMs,
|
|
10
|
-
} from "../src/installer.ts";
|
|
11
|
-
|
|
12
|
-
/** Environment keys this suite mutates; restored after every test. */
|
|
13
|
-
const KEYS = ["APPA_INSTALL_CMD", "APPA_INSTALL_TIMEOUT_MS"];
|
|
14
|
-
const saved: Record<string, string | undefined> = {};
|
|
15
|
-
for (const key of KEYS) saved[key] = process.env[key];
|
|
16
|
-
|
|
17
|
-
after(() => {
|
|
18
|
-
for (const key of KEYS) {
|
|
19
|
-
const value = saved[key];
|
|
20
|
-
if (value === undefined) delete process.env[key];
|
|
21
|
-
else process.env[key] = value;
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("installer resolution", () => {
|
|
26
|
-
it("defaults to the official install pipeline", () => {
|
|
27
|
-
delete process.env.APPA_INSTALL_CMD;
|
|
28
|
-
assert.equal(resolveInstallCmd(process.env), DEFAULT_INSTALL_CMD);
|
|
29
|
-
assert.equal(DEFAULT_INSTALL_CMD, "curl -fsSL https://openappa.com/install.sh | sh");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("honors APPA_INSTALL_CMD", () => {
|
|
33
|
-
process.env.APPA_INSTALL_CMD = "./offline-install.sh";
|
|
34
|
-
assert.equal(resolveInstallCmd(process.env), "./offline-install.sh");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("honors APPA_INSTALL_TIMEOUT_MS with a sane default", () => {
|
|
38
|
-
delete process.env.APPA_INSTALL_TIMEOUT_MS;
|
|
39
|
-
assert.equal(resolveInstallTimeoutMs(process.env), 120_000);
|
|
40
|
-
process.env.APPA_INSTALL_TIMEOUT_MS = "2500";
|
|
41
|
-
assert.equal(resolveInstallTimeoutMs(process.env), 2500);
|
|
42
|
-
process.env.APPA_INSTALL_TIMEOUT_MS = "garbage";
|
|
43
|
-
assert.equal(resolveInstallTimeoutMs(process.env), 120_000);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("installer run", () => {
|
|
48
|
-
it("captures a successful install", async () => {
|
|
49
|
-
process.env.APPA_INSTALL_CMD = "echo installed-ok";
|
|
50
|
-
const outcome = await installAppa();
|
|
51
|
-
assert.equal(outcome.exitCode, 0);
|
|
52
|
-
assert.ok(outcome.stdout.includes("installed-ok"));
|
|
53
|
-
assert.equal(outcome.timedOut, false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("captures a failed install with its output", async () => {
|
|
57
|
-
process.env.APPA_INSTALL_CMD = "echo boom >&2; exit 3";
|
|
58
|
-
const outcome = await installAppa();
|
|
59
|
-
assert.equal(outcome.exitCode, 3);
|
|
60
|
-
assert.ok(outcome.stderr.includes("boom"));
|
|
61
|
-
assert.equal(outcome.timedOut, false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("kills a stuck install", async () => {
|
|
65
|
-
process.env.APPA_INSTALL_CMD = "sleep 5";
|
|
66
|
-
process.env.APPA_INSTALL_TIMEOUT_MS = "150";
|
|
67
|
-
const started = Date.now();
|
|
68
|
-
const outcome = await installAppa();
|
|
69
|
-
assert.equal(outcome.timedOut, true);
|
|
70
|
-
assert.equal(outcome.exitCode, -1);
|
|
71
|
-
assert.ok(outcome.stderr.includes("timed out"));
|
|
72
|
-
assert.ok(Date.now() - started < 4000, "killed promptly, not after sleep 5");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("hook-client binary detection", () => {
|
|
77
|
-
it("flags a missing hook binary (ENOENT)", async () => {
|
|
78
|
-
const outcome = await invokeAppaHook(sessionStartPayload("s1", "startup", "/tmp"), {
|
|
79
|
-
bin: "/nonexistent-dir/appa",
|
|
80
|
-
});
|
|
81
|
-
assert.equal(outcome.binaryMissing, true);
|
|
82
|
-
assert.equal(outcome.exitCode, -1);
|
|
83
|
-
assert.ok(outcome.stderr.includes("failed to start"));
|
|
84
|
-
});
|
|
85
|
-
});
|