@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for HttpGitHubInstallationsReporter: change detection, heartbeat
|
|
3
|
+
* (injected Clock), payload hygiene, and never-throws behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import type { InstallationsSnapshot } from "./github-installations.ts";
|
|
8
|
+
import {
|
|
9
|
+
HttpGitHubInstallationsReporter,
|
|
10
|
+
INSTALLATIONS_HEARTBEAT_MS,
|
|
11
|
+
NoopGitHubInstallationsReporter,
|
|
12
|
+
startInstallationsHeartbeat,
|
|
13
|
+
} from "./github-installations-reporter.ts";
|
|
14
|
+
|
|
15
|
+
function snap(
|
|
16
|
+
health: "ok" | "broken" | "unknown" = "ok",
|
|
17
|
+
extra: InstallationsSnapshot["installations"] = [],
|
|
18
|
+
): InstallationsSnapshot {
|
|
19
|
+
return {
|
|
20
|
+
defaultId: 1,
|
|
21
|
+
installations: [
|
|
22
|
+
{
|
|
23
|
+
id: 1,
|
|
24
|
+
owner: "acme",
|
|
25
|
+
pinned: false,
|
|
26
|
+
health,
|
|
27
|
+
reason: health === "broken" ? "mint_rejected" : null,
|
|
28
|
+
},
|
|
29
|
+
...extra,
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function harness(responses: (Response | Error)[] = []) {
|
|
35
|
+
let nowMs = Date.UTC(2026, 0, 1);
|
|
36
|
+
const calls: { url: string; init: RequestInit | undefined }[] = [];
|
|
37
|
+
let i = 0;
|
|
38
|
+
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
calls.push({ url: String(input), init });
|
|
40
|
+
const r = responses[i++] ?? new Response("{}", { status: 200 });
|
|
41
|
+
if (r instanceof Error) throw r;
|
|
42
|
+
return r;
|
|
43
|
+
}) as unknown as typeof fetch;
|
|
44
|
+
const reporter = new HttpGitHubInstallationsReporter({
|
|
45
|
+
apiUrl: "http://api",
|
|
46
|
+
agentId: "a1",
|
|
47
|
+
apiKey: "KEY",
|
|
48
|
+
fetchFn,
|
|
49
|
+
clock: { now: () => new Date(nowMs) },
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
reporter,
|
|
53
|
+
calls,
|
|
54
|
+
advance: (ms: number) => {
|
|
55
|
+
nowMs += ms;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("HttpGitHubInstallationsReporter", () => {
|
|
61
|
+
test("first report PUTs a full snapshot", async () => {
|
|
62
|
+
const h = harness();
|
|
63
|
+
await h.reporter.report(snap());
|
|
64
|
+
expect(h.calls).toHaveLength(1);
|
|
65
|
+
expect(h.calls[0].url).toBe("http://api/agents/a1/github-installations");
|
|
66
|
+
expect(h.calls[0].init?.method).toBe("PUT");
|
|
67
|
+
const body = JSON.parse(String(h.calls[0].init?.body));
|
|
68
|
+
expect(body.installations).toEqual([
|
|
69
|
+
{ owner: "acme", installationId: 1, state: "ok", lastError: null },
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("unchanged state inside heartbeat window posts nothing", async () => {
|
|
74
|
+
const h = harness();
|
|
75
|
+
await h.reporter.report(snap());
|
|
76
|
+
h.advance(60_000);
|
|
77
|
+
await h.reporter.report(snap());
|
|
78
|
+
expect(h.calls).toHaveLength(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("ok -> broken -> ok each post", async () => {
|
|
82
|
+
const h = harness();
|
|
83
|
+
await h.reporter.report(snap("ok"));
|
|
84
|
+
await h.reporter.report(snap("broken"));
|
|
85
|
+
await h.reporter.report(snap("ok"));
|
|
86
|
+
expect(h.calls).toHaveLength(3);
|
|
87
|
+
const broken = JSON.parse(String(h.calls[1].init?.body));
|
|
88
|
+
expect(broken.installations[0].state).toBe("broken");
|
|
89
|
+
expect(broken.installations[0].lastError).toBe(
|
|
90
|
+
"installation token mint rejected",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("discovered and removed installations post", async () => {
|
|
95
|
+
const h = harness();
|
|
96
|
+
await h.reporter.report(snap());
|
|
97
|
+
const added = snap("ok", [
|
|
98
|
+
{ id: 2, owner: null, pinned: true, health: "unknown", reason: null },
|
|
99
|
+
]);
|
|
100
|
+
await h.reporter.report(added);
|
|
101
|
+
await h.reporter.report(snap());
|
|
102
|
+
expect(h.calls).toHaveLength(3);
|
|
103
|
+
expect(
|
|
104
|
+
JSON.parse(String(h.calls[1].init?.body)).installations[1].owner,
|
|
105
|
+
).toBe("unknown");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("heartbeat re-posts unchanged state after 30 minutes", async () => {
|
|
109
|
+
const h = harness();
|
|
110
|
+
await h.reporter.report(snap());
|
|
111
|
+
h.advance(INSTALLATIONS_HEARTBEAT_MS - 1);
|
|
112
|
+
await h.reporter.report(snap());
|
|
113
|
+
expect(h.calls).toHaveLength(1);
|
|
114
|
+
h.advance(1);
|
|
115
|
+
await h.reporter.report(snap());
|
|
116
|
+
expect(h.calls).toHaveLength(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("non-2xx and thrown fetch never throw, and retry next report", async () => {
|
|
120
|
+
const h = harness([
|
|
121
|
+
new Response("secret body", { status: 500 }),
|
|
122
|
+
new Error("boom"),
|
|
123
|
+
]);
|
|
124
|
+
await h.reporter.report(snap());
|
|
125
|
+
await h.reporter.report(snap());
|
|
126
|
+
await h.reporter.report(snap());
|
|
127
|
+
expect(h.calls).toHaveLength(3);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("payload carries no token, key, or response material", async () => {
|
|
131
|
+
const h = harness();
|
|
132
|
+
await h.reporter.report(snap("broken"));
|
|
133
|
+
const raw = String(h.calls[0].init?.body);
|
|
134
|
+
expect(Object.keys(JSON.parse(raw)).sort()).toEqual([
|
|
135
|
+
"installations",
|
|
136
|
+
"reportedAt",
|
|
137
|
+
]);
|
|
138
|
+
expect(raw).not.toMatch(/ghs_|BEGIN|"token"|"key"/);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("startInstallationsHeartbeat", () => {
|
|
143
|
+
test("ticks report the current state; stop clears the timer", async () => {
|
|
144
|
+
const reports: InstallationsSnapshot[] = [];
|
|
145
|
+
let tick: () => void = () => {};
|
|
146
|
+
const stop = startInstallationsHeartbeat(
|
|
147
|
+
{ report: async (s) => void reports.push(s) },
|
|
148
|
+
() => snap(),
|
|
149
|
+
{
|
|
150
|
+
setIntervalFn: ((fn: () => void) => {
|
|
151
|
+
tick = fn;
|
|
152
|
+
return 0;
|
|
153
|
+
}) as unknown as typeof setInterval,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
tick();
|
|
157
|
+
expect(reports).toHaveLength(1);
|
|
158
|
+
stop();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("NoopGitHubInstallationsReporter", () => {
|
|
163
|
+
test("resolves without effect", async () => {
|
|
164
|
+
await new NoopGitHubInstallationsReporter().report(snap());
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for discoverInstallations/selectInstallations using a
|
|
3
|
+
* recorded cassette of GET /app/installations (multi-page, a suspended
|
|
4
|
+
* installation, and a user installation not created via admin) replayed
|
|
5
|
+
* through an injected fetchFn. No real network.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
discoverInstallations,
|
|
13
|
+
selectInstallations,
|
|
14
|
+
} from "./github-installations.ts";
|
|
15
|
+
|
|
16
|
+
interface CassetteEntry {
|
|
17
|
+
status: number;
|
|
18
|
+
body: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cassette: Record<string, CassetteEntry> = JSON.parse(
|
|
22
|
+
readFileSync(
|
|
23
|
+
// Beside the runtime source when extracted, one level up in the monorepo.
|
|
24
|
+
[join(import.meta.dir, "fixtures"), join(import.meta.dir, "..", "fixtures")]
|
|
25
|
+
.map((dir) => join(dir, "github-installations", "installations.json"))
|
|
26
|
+
.find((path) => existsSync(path)) as string,
|
|
27
|
+
"utf-8",
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function replay(keyForPage: (page: number) => string) {
|
|
32
|
+
const requests: { url: string; auth: string | null }[] = [];
|
|
33
|
+
const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
34
|
+
const u = new URL(String(url));
|
|
35
|
+
requests.push({
|
|
36
|
+
url: String(url),
|
|
37
|
+
auth: new Headers(init?.headers).get("Authorization"),
|
|
38
|
+
});
|
|
39
|
+
const entry = cassette[keyForPage(Number(u.searchParams.get("page")))];
|
|
40
|
+
return new Response(JSON.stringify(entry.body), {
|
|
41
|
+
status: entry.status,
|
|
42
|
+
statusText: `status-${entry.status}`,
|
|
43
|
+
});
|
|
44
|
+
}) as typeof fetch;
|
|
45
|
+
return { fetchFn, requests };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("discoverInstallations (cassette)", () => {
|
|
49
|
+
it("follows pagination across pages with the App JWT only", async () => {
|
|
50
|
+
const { fetchFn, requests } = replay((p) =>
|
|
51
|
+
p === 1 ? "page1_full" : "page2_partial",
|
|
52
|
+
);
|
|
53
|
+
const r = await discoverInstallations("app-jwt", fetchFn);
|
|
54
|
+
expect(r.ok).toBe(true);
|
|
55
|
+
if (!r.ok) return;
|
|
56
|
+
expect(r.installations).toHaveLength(101);
|
|
57
|
+
expect(requests).toHaveLength(2);
|
|
58
|
+
expect(requests[0].url).toContain("per_page=100&page=1");
|
|
59
|
+
expect(requests.every((q) => q.auth === "Bearer app-jwt")).toBe(true);
|
|
60
|
+
expect(
|
|
61
|
+
r.installations.find((i) => i.id === 1005)?.suspendedAt,
|
|
62
|
+
).not.toBeNull();
|
|
63
|
+
expect(r.installations.find((i) => i.id === 2001)?.owner).toBe("octocat");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns an error result on a 401", async () => {
|
|
67
|
+
const { fetchFn } = replay(() => "unauthorized");
|
|
68
|
+
const r = await discoverInstallations("bad", fetchFn);
|
|
69
|
+
expect(r.ok).toBe(false);
|
|
70
|
+
if (!r.ok) expect(r.error).toContain("401");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("feeds selection: out-of-scope ignored, suspended flagged, non-admin install selected", async () => {
|
|
74
|
+
const { fetchFn } = replay((p) =>
|
|
75
|
+
p === 1 ? "page1_full" : "page2_partial",
|
|
76
|
+
);
|
|
77
|
+
const r = await discoverInstallations("jwt", fetchFn);
|
|
78
|
+
if (!r.ok) throw new Error("discovery failed");
|
|
79
|
+
const sel = selectInstallations({
|
|
80
|
+
discovered: r.installations,
|
|
81
|
+
scopedRepos: ["acme-corp/app", "suspended-org/x", "octocat/dotfiles"],
|
|
82
|
+
scopeSynced: true,
|
|
83
|
+
});
|
|
84
|
+
expect(sel.installations.map((i) => i.id)).toEqual([1003, 1005, 2001]);
|
|
85
|
+
expect(sel.installations.find((i) => i.id === 1005)?.suspended).toBe(true);
|
|
86
|
+
expect(sel.defaultId).toBe(1003);
|
|
87
|
+
});
|
|
88
|
+
});
|