@app-vitals/agent-runtime 0.0.0-stage → 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 +40 -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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/cron-failure-reporter.ts
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts's cron-sync scheduled callback so a thrown dispatch
|
|
5
|
+
* error (either the `dispatch === "loop"` path or the regular
|
|
6
|
+
* handleCronRequest path) surfaces the same way any other failed cron run
|
|
7
|
+
* does: an AgentCronRun row (visible in the admin run-history UI / Slack),
|
|
8
|
+
* not just raw pod stdout.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors loop-orchestrator.ts's dispatch() catch block: createRun() is
|
|
11
|
+
* called for the tick, then on a thrown error completeRun() is called with
|
|
12
|
+
* outcome "failed" and { error: <message> }. Also calls
|
|
13
|
+
* sentryClient.captureException(err) — this is a genuinely unhandled failure
|
|
14
|
+
* (the whole tick aborted), not a swallowed/handled one, so it's captured as
|
|
15
|
+
* a proper Sentry Issue rather than relying on consoleLoggingIntegration to
|
|
16
|
+
* turn a console.error call into a lower-signal Sentry log line.
|
|
17
|
+
*
|
|
18
|
+
* `cronRunReporter` and `sentryClient` are both optional-by-convention at the
|
|
19
|
+
* call site (NoopCronRunReporter / undefined respectively) — this function
|
|
20
|
+
* itself requires a CronRunReporter (callers pass the Noop fallback) and
|
|
21
|
+
* treats sentryClient as optional so it behaves identically whether or not
|
|
22
|
+
* SENTRY_DSN is set.
|
|
23
|
+
*
|
|
24
|
+
* index.ts's catch wraps BOTH the loop-dispatch path (which calls into
|
|
25
|
+
* loop-orchestrator.ts's dispatch()) and the handleCronRequest path
|
|
26
|
+
* (cron-handler.ts) — both of those already call
|
|
27
|
+
* cronRunReporter.completeRun(..., "failed", ...) on their own runId before
|
|
28
|
+
* rethrowing. To avoid a duplicate AgentCronRun "failed" row per real
|
|
29
|
+
* failure, those callers tag the rethrown error via
|
|
30
|
+
* markCronRunFailureReported() first; reportCronFailure() always logs and
|
|
31
|
+
* captures to Sentry, but skips its own createRun/completeRun pair when the
|
|
32
|
+
* error is already tagged.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { ErrorCapturingClient } from "@app-vitals/agent-lib/sentry";
|
|
36
|
+
import { reportClaudeError } from "./claude.ts";
|
|
37
|
+
import type { Clock } from "./clock.ts";
|
|
38
|
+
import type { CronRunReporter } from "./cron-run-reporter.ts";
|
|
39
|
+
|
|
40
|
+
interface ReportCronFailureDeps {
|
|
41
|
+
cronRunReporter: CronRunReporter;
|
|
42
|
+
sentryClient?: ErrorCapturingClient;
|
|
43
|
+
clock: Clock;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Symbol tag used to mark a thrown error as "an AgentCronRun failure row has
|
|
48
|
+
* already been recorded for this error" — set by callers (cron-handler.ts's
|
|
49
|
+
* runner-scope catch, loop-orchestrator.ts's dispatch() catch) that already
|
|
50
|
+
* called cronRunReporter.completeRun(..., "failed", ...) on their own runId
|
|
51
|
+
* before rethrowing. Symbol.for() (not Symbol()) so the tag survives across
|
|
52
|
+
* module instances if the error crosses a boundary.
|
|
53
|
+
*/
|
|
54
|
+
const CRON_RUN_ALREADY_REPORTED = Symbol.for(
|
|
55
|
+
"shipwright.cronRunAlreadyReported",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Marks a thrown error as already reported to CronRunReporter, so a later,
|
|
60
|
+
* broader catch (e.g. index.ts's cron-sync scheduled callback) can call
|
|
61
|
+
* reportCronFailure() for logging/Sentry purposes without also creating a
|
|
62
|
+
* duplicate AgentCronRun "failed" row.
|
|
63
|
+
*/
|
|
64
|
+
export function markCronRunFailureReported(err: unknown): void {
|
|
65
|
+
if (err && typeof err === "object") {
|
|
66
|
+
Reflect.set(err as object, CRON_RUN_ALREADY_REPORTED, true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isCronRunFailureReported(err: unknown): boolean {
|
|
71
|
+
return Boolean(
|
|
72
|
+
err &&
|
|
73
|
+
typeof err === "object" &&
|
|
74
|
+
Reflect.get(err as object, CRON_RUN_ALREADY_REPORTED),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reports a cron tick's dispatch failure: logs locally (console.error),
|
|
80
|
+
* captures the error in Sentry (when a sentryClient is wired), and records a
|
|
81
|
+
* failed AgentCronRun via createRun/completeRun so the failure shows up in
|
|
82
|
+
* the admin run-history UI and metrics dashboard, not just raw pod logs.
|
|
83
|
+
*/
|
|
84
|
+
export async function reportCronFailure(
|
|
85
|
+
cronId: string,
|
|
86
|
+
err: unknown,
|
|
87
|
+
deps: ReportCronFailureDeps,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const { cronRunReporter, sentryClient, clock } = deps;
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
|
|
92
|
+
console.error(`[cron] job ${cronId} failed:`, message);
|
|
93
|
+
|
|
94
|
+
reportClaudeError(sentryClient, err);
|
|
95
|
+
|
|
96
|
+
if (isCronRunFailureReported(err)) {
|
|
97
|
+
// A more specific layer (cron-handler.ts / loop-orchestrator.ts) already
|
|
98
|
+
// recorded a failed AgentCronRun on its own runId before rethrowing —
|
|
99
|
+
// skip creating a second, untagged row for the same failure.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const runId = await cronRunReporter.createRun(cronId, clock.now());
|
|
104
|
+
await cronRunReporter.completeRun(cronId, runId, clock.now(), "failed", {
|
|
105
|
+
error: message,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/cron-failure-reporter.unit.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for reportCronFailure() — pure logic, no I/O. Verifies a thrown
|
|
5
|
+
* cron dispatch error results in a createRun/completeRun("failed") pair on
|
|
6
|
+
* the injected CronRunReporter and a captureException call on the injected
|
|
7
|
+
* ErrorCapturingClient, mirroring loop-orchestrator.ts's dispatch() pattern.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { ClaudeTimeoutError } from "./claude.ts";
|
|
12
|
+
import { FixedClock } from "./clock.ts";
|
|
13
|
+
import {
|
|
14
|
+
markCronRunFailureReported,
|
|
15
|
+
reportCronFailure,
|
|
16
|
+
} from "./cron-failure-reporter.ts";
|
|
17
|
+
import type {
|
|
18
|
+
CronRunReporter,
|
|
19
|
+
ModelBreakdownEntry,
|
|
20
|
+
} from "./cron-run-reporter.ts";
|
|
21
|
+
|
|
22
|
+
const FIXED_TIME = new Date("2024-01-01T00:00:00.000Z");
|
|
23
|
+
|
|
24
|
+
interface CreateRunCall {
|
|
25
|
+
cronId: string;
|
|
26
|
+
startedAt: Date;
|
|
27
|
+
phase?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CompleteRunCall {
|
|
31
|
+
cronId: string;
|
|
32
|
+
runId: string | null;
|
|
33
|
+
completedAt: Date;
|
|
34
|
+
outcome: "completed" | "failed";
|
|
35
|
+
opts?: {
|
|
36
|
+
error?: string;
|
|
37
|
+
inputTokens?: number;
|
|
38
|
+
outputTokens?: number;
|
|
39
|
+
cacheReadTokens?: number;
|
|
40
|
+
cacheCreationTokens?: number;
|
|
41
|
+
modelBreakdown?: ModelBreakdownEntry[];
|
|
42
|
+
};
|
|
43
|
+
phase?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createFakeCronRunReporter(runId: string | null = "run-1") {
|
|
47
|
+
const createRunCalls: CreateRunCall[] = [];
|
|
48
|
+
const completeRunCalls: CompleteRunCall[] = [];
|
|
49
|
+
|
|
50
|
+
const reporter: CronRunReporter = {
|
|
51
|
+
async createRun(cronId, startedAt, phase) {
|
|
52
|
+
createRunCalls.push({ cronId, startedAt, phase });
|
|
53
|
+
return runId;
|
|
54
|
+
},
|
|
55
|
+
async completeRun(cronId, callRunId, completedAt, outcome, opts, phase) {
|
|
56
|
+
completeRunCalls.push({
|
|
57
|
+
cronId,
|
|
58
|
+
runId: callRunId,
|
|
59
|
+
completedAt,
|
|
60
|
+
outcome,
|
|
61
|
+
opts,
|
|
62
|
+
phase,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
async skipRun() {
|
|
66
|
+
throw new Error("skipRun should not be called by reportCronFailure");
|
|
67
|
+
},
|
|
68
|
+
async recordProgress() {},
|
|
69
|
+
async recordSessionId() {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { reporter, createRunCalls, completeRunCalls };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createFakeErrorCapturingClient() {
|
|
76
|
+
const capturedErrors: unknown[] = [];
|
|
77
|
+
const capturedMessages: string[] = [];
|
|
78
|
+
return {
|
|
79
|
+
client: {
|
|
80
|
+
captureException: (err: unknown) => {
|
|
81
|
+
capturedErrors.push(err);
|
|
82
|
+
},
|
|
83
|
+
captureMessage: (message: string) => {
|
|
84
|
+
capturedMessages.push(message);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
capturedErrors,
|
|
88
|
+
capturedMessages,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("reportCronFailure", () => {
|
|
93
|
+
it("creates a run and completes it as failed with the error message", async () => {
|
|
94
|
+
const { reporter, createRunCalls, completeRunCalls } =
|
|
95
|
+
createFakeCronRunReporter();
|
|
96
|
+
const clock = FixedClock(FIXED_TIME);
|
|
97
|
+
const err = new Error("dispatch exploded");
|
|
98
|
+
|
|
99
|
+
await reportCronFailure("my-cron-id", err, {
|
|
100
|
+
cronRunReporter: reporter,
|
|
101
|
+
clock,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(createRunCalls).toHaveLength(1);
|
|
105
|
+
expect(createRunCalls[0]?.cronId).toBe("my-cron-id");
|
|
106
|
+
expect(createRunCalls[0]?.startedAt).toEqual(FIXED_TIME);
|
|
107
|
+
|
|
108
|
+
expect(completeRunCalls).toHaveLength(1);
|
|
109
|
+
expect(completeRunCalls[0]?.cronId).toBe("my-cron-id");
|
|
110
|
+
expect(completeRunCalls[0]?.runId).toBe("run-1");
|
|
111
|
+
expect(completeRunCalls[0]?.completedAt).toEqual(FIXED_TIME);
|
|
112
|
+
expect(completeRunCalls[0]?.outcome).toBe("failed");
|
|
113
|
+
expect(completeRunCalls[0]?.opts?.error).toBe("dispatch exploded");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("stringifies a non-Error thrown value for the error field", async () => {
|
|
117
|
+
const { reporter, completeRunCalls } = createFakeCronRunReporter();
|
|
118
|
+
const clock = FixedClock(FIXED_TIME);
|
|
119
|
+
|
|
120
|
+
await reportCronFailure("my-cron-id", "just a string failure", {
|
|
121
|
+
cronRunReporter: reporter,
|
|
122
|
+
clock,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(completeRunCalls[0]?.outcome).toBe("failed");
|
|
126
|
+
expect(completeRunCalls[0]?.opts?.error).toBe("just a string failure");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("calls captureException on the injected sentry client with the original error", async () => {
|
|
130
|
+
const { reporter } = createFakeCronRunReporter();
|
|
131
|
+
const { client, capturedErrors } = createFakeErrorCapturingClient();
|
|
132
|
+
const clock = FixedClock(FIXED_TIME);
|
|
133
|
+
const err = new Error("boom");
|
|
134
|
+
|
|
135
|
+
await reportCronFailure("my-cron-id", err, {
|
|
136
|
+
cronRunReporter: reporter,
|
|
137
|
+
sentryClient: client,
|
|
138
|
+
clock,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(capturedErrors).toHaveLength(1);
|
|
142
|
+
expect(capturedErrors[0]).toBe(err);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("SENTRY-46: reports a ceiling-reason ClaudeTimeoutError via captureMessage, not captureException", async () => {
|
|
146
|
+
const { reporter } = createFakeCronRunReporter();
|
|
147
|
+
const { client, capturedErrors, capturedMessages } =
|
|
148
|
+
createFakeErrorCapturingClient();
|
|
149
|
+
const clock = FixedClock(FIXED_TIME);
|
|
150
|
+
const err = new ClaudeTimeoutError(3_600_000, "ceiling");
|
|
151
|
+
|
|
152
|
+
await reportCronFailure("my-cron-id", err, {
|
|
153
|
+
cronRunReporter: reporter,
|
|
154
|
+
sentryClient: client,
|
|
155
|
+
clock,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(capturedErrors).toHaveLength(0);
|
|
159
|
+
expect(capturedMessages).toHaveLength(1);
|
|
160
|
+
expect(capturedMessages[0]).toContain("3600s");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("still reports an idle-reason ClaudeTimeoutError via captureException", async () => {
|
|
164
|
+
const { reporter } = createFakeCronRunReporter();
|
|
165
|
+
const { client, capturedErrors, capturedMessages } =
|
|
166
|
+
createFakeErrorCapturingClient();
|
|
167
|
+
const clock = FixedClock(FIXED_TIME);
|
|
168
|
+
const err = new ClaudeTimeoutError(1_500_000, "idle");
|
|
169
|
+
|
|
170
|
+
await reportCronFailure("my-cron-id", err, {
|
|
171
|
+
cronRunReporter: reporter,
|
|
172
|
+
sentryClient: client,
|
|
173
|
+
clock,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(capturedMessages).toHaveLength(0);
|
|
177
|
+
expect(capturedErrors).toHaveLength(1);
|
|
178
|
+
expect(capturedErrors[0]).toBe(err);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("does not throw when no sentryClient is provided", async () => {
|
|
182
|
+
const { reporter } = createFakeCronRunReporter();
|
|
183
|
+
const clock = FixedClock(FIXED_TIME);
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
reportCronFailure("my-cron-id", new Error("boom"), {
|
|
187
|
+
cronRunReporter: reporter,
|
|
188
|
+
clock,
|
|
189
|
+
}),
|
|
190
|
+
).resolves.toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("still creates/completes a run for an error that was not marked as already-reported", async () => {
|
|
194
|
+
const { reporter, createRunCalls, completeRunCalls } =
|
|
195
|
+
createFakeCronRunReporter();
|
|
196
|
+
const clock = FixedClock(FIXED_TIME);
|
|
197
|
+
const err = new Error("unmarked failure");
|
|
198
|
+
|
|
199
|
+
await reportCronFailure("my-cron-id", err, {
|
|
200
|
+
cronRunReporter: reporter,
|
|
201
|
+
clock,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(createRunCalls).toHaveLength(1);
|
|
205
|
+
expect(completeRunCalls).toHaveLength(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("skips createRun/completeRun but still logs and captures to Sentry when the error was already reported", async () => {
|
|
209
|
+
const { reporter, createRunCalls, completeRunCalls } =
|
|
210
|
+
createFakeCronRunReporter();
|
|
211
|
+
const { client, capturedErrors } = createFakeErrorCapturingClient();
|
|
212
|
+
const clock = FixedClock(FIXED_TIME);
|
|
213
|
+
const err = new Error("already reported by inner catch");
|
|
214
|
+
markCronRunFailureReported(err);
|
|
215
|
+
|
|
216
|
+
await reportCronFailure("my-cron-id", err, {
|
|
217
|
+
cronRunReporter: reporter,
|
|
218
|
+
sentryClient: client,
|
|
219
|
+
clock,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(createRunCalls).toHaveLength(0);
|
|
223
|
+
expect(completeRunCalls).toHaveLength(0);
|
|
224
|
+
expect(capturedErrors).toHaveLength(1);
|
|
225
|
+
expect(capturedErrors[0]).toBe(err);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does not mark non-object thrown values (e.g. strings) but still reports normally", async () => {
|
|
229
|
+
const { reporter, createRunCalls, completeRunCalls } =
|
|
230
|
+
createFakeCronRunReporter();
|
|
231
|
+
|
|
232
|
+
// markCronRunFailureReported on a primitive is a no-op; should not throw.
|
|
233
|
+
expect(() => markCronRunFailureReported("just a string")).not.toThrow();
|
|
234
|
+
|
|
235
|
+
const clock = FixedClock(FIXED_TIME);
|
|
236
|
+
await reportCronFailure("my-cron-id", "just a string", {
|
|
237
|
+
cronRunReporter: reporter,
|
|
238
|
+
clock,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(createRunCalls).toHaveLength(1);
|
|
242
|
+
expect(completeRunCalls).toHaveLength(1);
|
|
243
|
+
});
|
|
244
|
+
});
|