@app-vitals/agent-runtime 0.0.1 → 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/Dockerfile +265 -0
- package/agent-account-status-ref.ts +53 -0
- package/agent-account-status-ref.unit.test.ts +24 -0
- package/agent-extension.ts +144 -0
- package/agent-extension.unit.test.ts +139 -0
- package/agent-repos-ref.ts +63 -0
- package/agent-repos-ref.unit.test.ts +84 -0
- package/agent-slack-membership-ref.ts +94 -0
- package/agent-slack-membership-ref.unit.test.ts +201 -0
- package/agent-trial-expiry-ref.ts +63 -0
- package/agent-trial-expiry-ref.unit.test.ts +84 -0
- package/anthropic-federation-guard.ts +72 -0
- package/anthropic-federation-guard.unit.test.ts +127 -0
- package/base-image-contract.smoke.test.ts +149 -0
- package/boot.ts +845 -0
- package/browser.integration.test.ts +52 -0
- package/browser.ts +34 -0
- package/chat-poller.integration.test.ts +109 -0
- package/chat-poller.ts +401 -0
- package/chat-poller.unit.test.ts +1581 -0
- package/chat-token-reporter.integration.test.ts +282 -0
- package/chat-token-reporter.ts +136 -0
- package/claude-version.ts +55 -0
- package/claude-version.unit.test.ts +73 -0
- package/claude.ts +1054 -0
- package/claude.unit.test.ts +3042 -0
- package/cleanup-after-days-ref.ts +83 -0
- package/cleanup-after-days-ref.unit.test.ts +89 -0
- package/cleanup-merged-worktrees-ref.ts +75 -0
- package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
- package/cli-args.ts +36 -0
- package/cli-args.unit.test.ts +94 -0
- package/clock.ts +47 -0
- package/config.ts +99 -0
- package/config.unit.test.ts +356 -0
- package/context-stamp.ts +189 -0
- package/context-stamp.unit.test.ts +159 -0
- package/cron-failure-reporter.ts +107 -0
- package/cron-failure-reporter.unit.test.ts +244 -0
- package/cron-handler.integration.test.ts +614 -0
- package/cron-handler.smoke.test.ts +1371 -0
- package/cron-handler.ts +750 -0
- package/cron-handler.unit.test.ts +1177 -0
- package/cron-run-reporter.integration.test.ts +806 -0
- package/cron-run-reporter.ts +398 -0
- package/crypto.ts +73 -0
- package/cutover-validate.integration.test.ts +245 -0
- package/cutover-validate.ts +84 -0
- package/entrypoint-main.ts +79 -0
- package/entrypoint.integration.test.ts +315 -0
- package/entrypoint.ts +154 -0
- package/errors.ts +61 -0
- package/fixtures/chat-service/attachment.json +15 -0
- package/fixtures/chat-service/messages.json +86 -0
- package/fixtures/chat-service/threads.json +50 -0
- package/fixtures/github-installations/installations.json +1231 -0
- package/fixtures/slack-provision-cassette.json +6 -0
- package/fixtures/specialties/fixture/manifest.yaml +35 -0
- package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
- package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
- package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
- package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
- package/fixtures/stream-json/clean-multi-turn.ts +209 -0
- package/fixtures/stream-json/progress-phases.ts +203 -0
- package/fixtures/stream-json/skill-attribution.ts +251 -0
- package/fixtures/stream-json/truncated-no-result.ts +84 -0
- package/format.ts +283 -0
- package/format.unit.test.ts +558 -0
- package/gh-helpers.ts +139 -0
- package/gh-helpers.unit.test.ts +253 -0
- package/gh-token-files.integration.test.ts +143 -0
- package/gh-token-files.ts +92 -0
- package/gh-token-files.unit.test.ts +63 -0
- package/github-app-auth.integration.test.ts +419 -0
- package/github-app-auth.ts +193 -0
- package/github-auth-deps.ts +128 -0
- package/github-auth-startup.ts +231 -0
- package/github-auth-startup.unit.test.ts +325 -0
- package/github-installations-manager.integration.test.ts +323 -0
- package/github-installations-reporter.integration.test.ts +124 -0
- package/github-installations-reporter.ts +150 -0
- package/github-installations-reporter.unit.test.ts +166 -0
- package/github-installations.integration.test.ts +88 -0
- package/github-installations.ts +462 -0
- package/github-installations.unit.test.ts +98 -0
- package/health.smoke.test.ts +244 -0
- package/health.ts +193 -0
- package/health.unit.test.ts +40 -0
- package/http-chat-service-client.integration.test.ts +345 -0
- package/http-chat-service-client.ts +287 -0
- package/http-chat-service-client.unit.test.ts +500 -0
- package/log-prefix.ts +12 -0
- package/log-prefix.unit.test.ts +24 -0
- package/markers.ts +178 -0
- package/markers.unit.test.ts +374 -0
- package/package.json +38 -4
- package/piper-voice.ts +116 -0
- package/piper-voice.unit.test.ts +124 -0
- package/pricing.ts +7 -0
- package/pricing.unit.test.ts +149 -0
- package/process-tree-kill.integration.test.ts +133 -0
- package/process-tree-kill.ts +325 -0
- package/process-tree-kill.unit.test.ts +265 -0
- package/progress-milestones.ts +86 -0
- package/progress-milestones.unit.test.ts +188 -0
- package/run-telemetry.ts +241 -0
- package/run-telemetry.unit.test.ts +235 -0
- package/sessions.integration.test.ts +273 -0
- package/sessions.ts +191 -0
- package/sessions.unit.test.ts +94 -0
- package/setup-github-auth.integration.test.ts +858 -0
- package/setup-github-auth.ts +297 -0
- package/setup.fixture.integration.test.ts +109 -0
- package/setup.integration.test.ts +1495 -0
- package/setup.ts +853 -0
- package/setup.unit.test.ts +286 -0
- package/shipwright-config-client.ts +36 -0
- package/shipwright-runtime-client.integration.test.ts +336 -0
- package/shipwright-runtime-client.ts +118 -0
- package/slack-account-status.unit.test.ts +251 -0
- package/slack-bolt-client.integration.test.ts +76 -0
- package/slack-chat-conversation.integration.test.ts +333 -0
- package/slack-progress.ts +510 -0
- package/slack-progress.unit.test.ts +806 -0
- package/slack-socket-mode-ping.integration.test.ts +137 -0
- package/slack-startup.ts +170 -0
- package/slack-startup.unit.test.ts +300 -0
- package/slack-thread-status-tracker.ts +70 -0
- package/slack-thread-status-tracker.unit.test.ts +118 -0
- package/slack-trial-expiry.unit.test.ts +269 -0
- package/slack.integration.test.ts +3979 -0
- package/slack.ts +1286 -0
- package/slack.unit.test.ts +1070 -0
- package/spawn-env.ts +58 -0
- package/spawn-env.unit.test.ts +96 -0
- package/startup-dm.integration.test.ts +156 -0
- package/startup-dm.ts +29 -0
- package/sync-config-clone.ts +238 -0
- package/sync-config-clone.unit.test.ts +429 -0
- package/test-env.ts +27 -0
- package/users.ts +90 -0
- package/users.unit.test.ts +108 -0
- package/voice.integration.test.ts +669 -0
- package/voice.ts +315 -0
- package/voice.unit.test.ts +105 -0
- package/workspace/BOOTSTRAP.md.template +43 -0
- package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
- package/workspace/IDENTITY.md.template +8 -0
- package/workspace/RUNTIME.md.template +193 -0
- package/workspace/RUNTIME.md.template.content.test.ts +75 -0
- package/workspace/SOUL.md.template +45 -0
- package/workspace/SPECIALTY.md.template +37 -0
- package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
- package/workspace/VOICE.md.template +8 -0
- package/workspace/mise.toml.template +12 -0
- package/workspace/requirements.txt.template +1 -0
- package/workspace-paths.ts +37 -0
- package/worktree-reaper.ts +244 -0
- package/worktree-reaper.unit.test.ts +380 -0
- package/README.md +0 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/anthropic-federation-guard.unit.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the WIF-2.1 defense-in-depth filter: with
|
|
5
|
+
* ANTHROPIC_FEDERATION_RULE_ID in the pod env, the three Anthropic credential
|
|
6
|
+
* keys never reach process.env, whether via startup applyEnv or syncConfig.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
ANTHROPIC_CREDENTIAL_KEYS,
|
|
12
|
+
applyEnvToProcess,
|
|
13
|
+
filterAnthropicCredentials,
|
|
14
|
+
} from "./anthropic-federation-guard.ts";
|
|
15
|
+
|
|
16
|
+
const CREDS = {
|
|
17
|
+
CLAUDE_CODE_OAUTH_TOKEN: "oat-secret",
|
|
18
|
+
ANTHROPIC_API_KEY: "api-secret",
|
|
19
|
+
ANTHROPIC_AUTH_TOKEN: "auth-secret",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function setup(federated: boolean, initial: Record<string, string> = {}) {
|
|
23
|
+
const processEnv: Record<string, string | undefined> = {
|
|
24
|
+
...(federated ? { ANTHROPIC_FEDERATION_RULE_ID: "rule-1" } : {}),
|
|
25
|
+
...initial,
|
|
26
|
+
};
|
|
27
|
+
const logged = new Set<string>();
|
|
28
|
+
const logs: string[] = [];
|
|
29
|
+
const log = (m: string) => logs.push(m);
|
|
30
|
+
return { processEnv, logged, logs, log };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("filterAnthropicCredentials", () => {
|
|
34
|
+
it("lists exactly the three blocked keys", () => {
|
|
35
|
+
expect([...ANTHROPIC_CREDENTIAL_KEYS].sort()).toEqual([
|
|
36
|
+
"ANTHROPIC_API_KEY",
|
|
37
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
38
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns env unchanged when federation is not configured", () => {
|
|
43
|
+
const s = setup(false);
|
|
44
|
+
const env = { FOO: "bar", ...CREDS };
|
|
45
|
+
expect(filterAnthropicCredentials(env, s)).toEqual(env);
|
|
46
|
+
expect(s.logs).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("drops the three keys and keeps the rest when federated", () => {
|
|
50
|
+
const s = setup(true);
|
|
51
|
+
expect(filterAnthropicCredentials({ FOO: "bar", ...CREDS }, s)).toEqual({
|
|
52
|
+
FOO: "bar",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("also drops empty-string values (they still outrank federation)", () => {
|
|
57
|
+
const s = setup(true);
|
|
58
|
+
expect(
|
|
59
|
+
filterAnthropicCredentials({ ANTHROPIC_API_KEY: "", FOO: "x" }, s),
|
|
60
|
+
).toEqual({ FOO: "x" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("logs once per key, names only, never values", () => {
|
|
64
|
+
const s = setup(true);
|
|
65
|
+
filterAnthropicCredentials({ ...CREDS }, s);
|
|
66
|
+
filterAnthropicCredentials({ ...CREDS }, s);
|
|
67
|
+
expect(s.logs).toHaveLength(3);
|
|
68
|
+
const all = s.logs.join("\n");
|
|
69
|
+
for (const k of ANTHROPIC_CREDENTIAL_KEYS) expect(all).toContain(k);
|
|
70
|
+
for (const v of Object.values(CREDS)) expect(all).not.toContain(v);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("deletes the keys from process.env when federated", () => {
|
|
74
|
+
const s = setup(true, { ...CREDS, FOO: "keep" });
|
|
75
|
+
filterAnthropicCredentials({}, s);
|
|
76
|
+
expect(s.processEnv.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
77
|
+
expect(s.processEnv.ANTHROPIC_API_KEY).toBeUndefined();
|
|
78
|
+
expect(s.processEnv.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
79
|
+
expect(s.processEnv.FOO).toBe("keep");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("leaves process.env alone when federation is not configured", () => {
|
|
83
|
+
const s = setup(false, { ANTHROPIC_API_KEY: "mine" });
|
|
84
|
+
filterAnthropicCredentials({}, s);
|
|
85
|
+
expect(s.processEnv.ANTHROPIC_API_KEY).toBe("mine");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("applyEnvToProcess", () => {
|
|
90
|
+
it("applies all keys when not federated", () => {
|
|
91
|
+
const s = setup(false);
|
|
92
|
+
applyEnvToProcess({ FOO: "bar", ...CREDS }, s);
|
|
93
|
+
expect(s.processEnv).toMatchObject({ FOO: "bar", ...CREDS });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("startup: never leaves the three keys in process.env when federated", () => {
|
|
97
|
+
const s = setup(true, { ANTHROPIC_API_KEY: "from-pod" });
|
|
98
|
+
applyEnvToProcess({ FOO: "bar", ...CREDS }, s);
|
|
99
|
+
expect(s.processEnv.FOO).toBe("bar");
|
|
100
|
+
for (const k of ANTHROPIC_CREDENTIAL_KEYS) {
|
|
101
|
+
expect(s.processEnv[k]).toBeUndefined();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("config sync: a later bundle containing the keys is filtered too", () => {
|
|
106
|
+
const s = setup(true);
|
|
107
|
+
applyEnvToProcess({ FOO: "1" }, s);
|
|
108
|
+
applyEnvToProcess({ FOO: "2", ANTHROPIC_AUTH_TOKEN: "late" }, s);
|
|
109
|
+
expect(s.processEnv.FOO).toBe("2");
|
|
110
|
+
expect(s.processEnv.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("config sync: reports changed keys, never a blocked credential key", () => {
|
|
114
|
+
const s = setup(true, { FOO: "same" });
|
|
115
|
+
const changed = applyEnvToProcess({ FOO: "same", BAR: "new", ...CREDS }, s);
|
|
116
|
+
expect(changed).toEqual(["BAR"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("config sync: reports changed credential keys when not federated", () => {
|
|
120
|
+
const s = setup(false, { ANTHROPIC_API_KEY: "api-secret" });
|
|
121
|
+
const changed = applyEnvToProcess({ ...CREDS }, s);
|
|
122
|
+
expect(changed).toEqual([
|
|
123
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
124
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-image contract guard (ARD-2.1).
|
|
3
|
+
*
|
|
4
|
+
* Downstream images build FROM the shipwright-agent image and
|
|
5
|
+
* COPY a marketplace to the /opt/shipwright/marketplaces/ convention root. This
|
|
6
|
+
* test builds such a derived image the same way and asserts the contract:
|
|
7
|
+
* - a marketplace at the convention root registers at boot via installPlugins()
|
|
8
|
+
* - /app/runtime/entrypoint-main.ts exists and runs under `bun run`
|
|
9
|
+
* - /app/scripts/bin/gh, git and the git-credential helper are executable, ahead of the real ones on PATH
|
|
10
|
+
*
|
|
11
|
+
* Needs a Docker daemon and an already-built base image, so it is image-level
|
|
12
|
+
* smoke and opt-in: set SHIPWRIGHT_CONTRACT_BASE_IMAGE (CI sets it to the image
|
|
13
|
+
* built by the `agent docker build` job). Skipped otherwise.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
17
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const BASE_IMAGE = process.env.SHIPWRIGHT_CONTRACT_BASE_IMAGE;
|
|
22
|
+
const CONVENTION_ROOT = "/opt/shipwright/marketplaces";
|
|
23
|
+
const MARKETPLACE_NAME = "contract-test";
|
|
24
|
+
const DERIVED_TAG = `shipwright-agent-contract:${process.pid}`;
|
|
25
|
+
|
|
26
|
+
function docker(args: string[]) {
|
|
27
|
+
const proc = Bun.spawnSync(["docker", ...args], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
exitCode: proc.exitCode,
|
|
33
|
+
stdout: proc.stdout.toString(),
|
|
34
|
+
stderr: proc.stderr.toString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe.skipIf(!BASE_IMAGE)("agent base-image contract", () => {
|
|
39
|
+
let contextDir: string;
|
|
40
|
+
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
contextDir = mkdtempSync(join(tmpdir(), "base-image-contract-"));
|
|
43
|
+
const marketplaceDir = join(contextDir, "marketplace", ".claude-plugin");
|
|
44
|
+
mkdirSync(marketplaceDir, { recursive: true });
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(marketplaceDir, "marketplace.json"),
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
name: MARKETPLACE_NAME,
|
|
49
|
+
owner: { name: "contract-test" },
|
|
50
|
+
plugins: [],
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(contextDir, "Dockerfile"),
|
|
55
|
+
[
|
|
56
|
+
`FROM ${BASE_IMAGE}`,
|
|
57
|
+
"USER root",
|
|
58
|
+
`COPY marketplace ${CONVENTION_ROOT}/${MARKETPLACE_NAME}/`,
|
|
59
|
+
"USER 1000",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
);
|
|
63
|
+
const build = docker(["build", "-t", DERIVED_TAG, contextDir]);
|
|
64
|
+
expect(build.exitCode, build.stderr).toBe(0);
|
|
65
|
+
}, 300_000);
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
docker(["rmi", "-f", DERIVED_TAG]);
|
|
69
|
+
if (contextDir) rmSync(contextDir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("installPlugins() registers a marketplace baked at the convention root", () => {
|
|
73
|
+
// Recording exec: asserts what installPlugins() asks `claude` to do without
|
|
74
|
+
// needing network access or a logged-in CLI inside the container.
|
|
75
|
+
const script = `
|
|
76
|
+
import { installPlugins } from "/app/runtime/setup.ts";
|
|
77
|
+
const calls = [];
|
|
78
|
+
const exec = async (cmd, args) => {
|
|
79
|
+
calls.push([cmd, ...args]);
|
|
80
|
+
return { stdout: "", exitCode: 0 };
|
|
81
|
+
};
|
|
82
|
+
await installPlugins(exec, "/tmp", [], undefined, "/nonexistent/installed_plugins.json");
|
|
83
|
+
console.log("CALLS:" + JSON.stringify(calls));
|
|
84
|
+
`;
|
|
85
|
+
const run = docker([
|
|
86
|
+
"run",
|
|
87
|
+
"--rm",
|
|
88
|
+
"--entrypoint",
|
|
89
|
+
"bun",
|
|
90
|
+
DERIVED_TAG,
|
|
91
|
+
"-e",
|
|
92
|
+
script,
|
|
93
|
+
]);
|
|
94
|
+
expect(run.exitCode, run.stderr).toBe(0);
|
|
95
|
+
|
|
96
|
+
const line = run.stdout.split("\n").find((l) => l.startsWith("CALLS:"));
|
|
97
|
+
expect(line, run.stdout).toBeDefined();
|
|
98
|
+
const calls: string[][] = JSON.parse(
|
|
99
|
+
(line as string).slice("CALLS:".length),
|
|
100
|
+
);
|
|
101
|
+
const adds = calls
|
|
102
|
+
.filter(
|
|
103
|
+
(c) => c.slice(0, 4).join(" ") === "claude plugin marketplace add",
|
|
104
|
+
)
|
|
105
|
+
.map((c) => c[4]);
|
|
106
|
+
|
|
107
|
+
expect(adds).toContain(`${CONVENTION_ROOT}/${MARKETPLACE_NAME}`);
|
|
108
|
+
}, 60_000);
|
|
109
|
+
|
|
110
|
+
test("entrypoint-main.ts exists and runs under `bun run`", () => {
|
|
111
|
+
// No agent id/url/key: runEntrypoint() reports the missing vars and exits 1.
|
|
112
|
+
// A missing file or broken import graph fails differently ("Module not
|
|
113
|
+
// found"), so the message proves the entrypoint actually executed.
|
|
114
|
+
const run = docker([
|
|
115
|
+
"run",
|
|
116
|
+
"--rm",
|
|
117
|
+
"--entrypoint",
|
|
118
|
+
"bun",
|
|
119
|
+
"-e",
|
|
120
|
+
"SHIPWRIGHT_AGENT_ID=",
|
|
121
|
+
"-e",
|
|
122
|
+
"SHIPWRIGHT_API_URL=",
|
|
123
|
+
"-e",
|
|
124
|
+
"SHIPWRIGHT_AGENT_API_KEY=",
|
|
125
|
+
DERIVED_TAG,
|
|
126
|
+
"run",
|
|
127
|
+
"/app/runtime/entrypoint-main.ts",
|
|
128
|
+
]);
|
|
129
|
+
expect(run.exitCode).toBe(1);
|
|
130
|
+
expect(run.stdout + run.stderr).toContain("SHIPWRIGHT_AGENT_ID");
|
|
131
|
+
}, 60_000);
|
|
132
|
+
|
|
133
|
+
test("scripts/bin holds executable gh, git and git-credential wrappers ahead of the real gh and git on PATH", () => {
|
|
134
|
+
const run = docker([
|
|
135
|
+
"run",
|
|
136
|
+
"--rm",
|
|
137
|
+
"--entrypoint",
|
|
138
|
+
"bash",
|
|
139
|
+
DERIVED_TAG,
|
|
140
|
+
"-c",
|
|
141
|
+
"test -x /app/scripts/bin/gh && test -x /app/scripts/bin/git && test -x /app/scripts/bin/git-credential-shipwright.sh && command -v gh git",
|
|
142
|
+
]);
|
|
143
|
+
expect(run.exitCode, run.stderr).toBe(0);
|
|
144
|
+
expect(run.stdout.trim().split("\n")).toEqual([
|
|
145
|
+
"/app/scripts/bin/gh",
|
|
146
|
+
"/app/scripts/bin/git",
|
|
147
|
+
]);
|
|
148
|
+
}, 60_000);
|
|
149
|
+
});
|