@urateam/core 0.1.32 → 0.1.34
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/dist/__tests__/audit-immutability.test.js +50 -1
- package/dist/__tests__/audit-immutability.test.js.map +1 -1
- package/dist/__tests__/auth-monitor.test.d.ts +2 -0
- package/dist/__tests__/auth-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/auth-monitor.test.js +253 -0
- package/dist/__tests__/auth-monitor.test.js.map +1 -0
- package/dist/__tests__/bec-186-repro.test.d.ts +16 -0
- package/dist/__tests__/bec-186-repro.test.d.ts.map +1 -0
- package/dist/__tests__/bec-186-repro.test.js +223 -0
- package/dist/__tests__/bec-186-repro.test.js.map +1 -0
- package/dist/__tests__/db-migrations.test.d.ts +2 -0
- package/dist/__tests__/db-migrations.test.d.ts.map +1 -0
- package/dist/__tests__/db-migrations.test.js +237 -0
- package/dist/__tests__/db-migrations.test.js.map +1 -0
- package/dist/__tests__/executor-issue-id.test.js +2 -0
- package/dist/__tests__/executor-issue-id.test.js.map +1 -1
- package/dist/__tests__/pm-scheduler.test.js +59 -0
- package/dist/__tests__/pm-scheduler.test.js.map +1 -1
- package/dist/__tests__/post-fanout-comments.test.js +36 -0
- package/dist/__tests__/post-fanout-comments.test.js.map +1 -1
- package/dist/__tests__/preflight-claude-auth.test.d.ts +2 -0
- package/dist/__tests__/preflight-claude-auth.test.d.ts.map +1 -0
- package/dist/__tests__/preflight-claude-auth.test.js +36 -0
- package/dist/__tests__/preflight-claude-auth.test.js.map +1 -0
- package/dist/__tests__/resolve-claude-auth.test.d.ts +2 -0
- package/dist/__tests__/resolve-claude-auth.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-claude-auth.test.js +129 -0
- package/dist/__tests__/resolve-claude-auth.test.js.map +1 -0
- package/dist/__tests__/runner-retry-strategies.test.d.ts +23 -0
- package/dist/__tests__/runner-retry-strategies.test.d.ts.map +1 -0
- package/dist/__tests__/runner-retry-strategies.test.js +274 -0
- package/dist/__tests__/runner-retry-strategies.test.js.map +1 -0
- package/dist/__tests__/scratch-file-guard.test.d.ts +2 -0
- package/dist/__tests__/scratch-file-guard.test.d.ts.map +1 -0
- package/dist/__tests__/scratch-file-guard.test.js +144 -0
- package/dist/__tests__/scratch-file-guard.test.js.map +1 -0
- package/dist/__tests__/spec-vs-impl-gate.test.d.ts +2 -0
- package/dist/__tests__/spec-vs-impl-gate.test.d.ts.map +1 -0
- package/dist/__tests__/spec-vs-impl-gate.test.js +222 -0
- package/dist/__tests__/spec-vs-impl-gate.test.js.map +1 -0
- package/dist/__tests__/stage-models.test.js +4 -0
- package/dist/__tests__/stage-models.test.js.map +1 -1
- package/dist/__tests__/typecheck-gate.test.d.ts +2 -0
- package/dist/__tests__/typecheck-gate.test.d.ts.map +1 -0
- package/dist/__tests__/typecheck-gate.test.js +196 -0
- package/dist/__tests__/typecheck-gate.test.js.map +1 -0
- package/dist/audit/events.d.ts +52 -0
- package/dist/audit/events.d.ts.map +1 -1
- package/dist/audit/events.js +81 -0
- package/dist/audit/events.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +8 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrations/postgres/014_missing_indexes.sql +28 -0
- package/dist/db/migrations/sqlite/013_missing_indexes.sql +28 -0
- package/dist/executor/auth-check.d.ts +39 -0
- package/dist/executor/auth-check.d.ts.map +1 -1
- package/dist/executor/auth-check.js +31 -0
- package/dist/executor/auth-check.js.map +1 -1
- package/dist/executor/auth-monitor.d.ts +40 -0
- package/dist/executor/auth-monitor.d.ts.map +1 -0
- package/dist/executor/auth-monitor.js +114 -0
- package/dist/executor/auth-monitor.js.map +1 -0
- package/dist/executor/executor.d.ts.map +1 -1
- package/dist/executor/executor.js +12 -26
- package/dist/executor/executor.js.map +1 -1
- package/dist/executor/index.d.ts +2 -0
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +2 -0
- package/dist/executor/index.js.map +1 -1
- package/dist/executor/review/post-fanout-comments.d.ts +8 -0
- package/dist/executor/review/post-fanout-comments.d.ts.map +1 -1
- package/dist/executor/review/post-fanout-comments.js +23 -3
- package/dist/executor/review/post-fanout-comments.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/notifier/composite.d.ts +1 -0
- package/dist/notifier/composite.d.ts.map +1 -1
- package/dist/notifier/composite.js +3 -0
- package/dist/notifier/composite.js.map +1 -1
- package/dist/notifier/linear.d.ts +2 -0
- package/dist/notifier/linear.d.ts.map +1 -1
- package/dist/notifier/linear.js +12 -2
- package/dist/notifier/linear.js.map +1 -1
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +152 -2
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pipeline/scratch-file-guard.d.ts +21 -0
- package/dist/pipeline/scratch-file-guard.d.ts.map +1 -0
- package/dist/pipeline/scratch-file-guard.js +155 -0
- package/dist/pipeline/scratch-file-guard.js.map +1 -0
- package/dist/pipeline/spec-vs-impl-gate.d.ts +49 -0
- package/dist/pipeline/spec-vs-impl-gate.d.ts.map +1 -0
- package/dist/pipeline/spec-vs-impl-gate.js +177 -0
- package/dist/pipeline/spec-vs-impl-gate.js.map +1 -0
- package/dist/pipeline/typecheck-gate.d.ts +34 -0
- package/dist/pipeline/typecheck-gate.d.ts.map +1 -0
- package/dist/pipeline/typecheck-gate.js +89 -0
- package/dist/pipeline/typecheck-gate.js.map +1 -0
- package/dist/pm/scheduler.d.ts.map +1 -1
- package/dist/pm/scheduler.js +19 -0
- package/dist/pm/scheduler.js.map +1 -1
- package/dist/release-manager/index.d.ts +2 -0
- package/dist/release-manager/index.d.ts.map +1 -1
- package/dist/release-manager/index.js +2 -0
- package/dist/release-manager/index.js.map +1 -1
- package/dist/release-manager/release-helpers.d.ts +112 -0
- package/dist/release-manager/release-helpers.d.ts.map +1 -0
- package/dist/release-manager/release-helpers.js +164 -0
- package/dist/release-manager/release-helpers.js.map +1 -0
- package/dist/release-manager/release-tick.d.ts +101 -0
- package/dist/release-manager/release-tick.d.ts.map +1 -0
- package/dist/release-manager/release-tick.js +374 -0
- package/dist/release-manager/release-tick.js.map +1 -0
- package/dist/release-manager/scheduler.d.ts +28 -3
- package/dist/release-manager/scheduler.d.ts.map +1 -1
- package/dist/release-manager/scheduler.js +41 -417
- package/dist/release-manager/scheduler.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -1
- package/dist/webhook/github-handler.d.ts +7 -1
- package/dist/webhook/github-handler.d.ts.map +1 -1
- package/dist/webhook/github-handler.js +82 -38
- package/dist/webhook/github-handler.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { describe, it, expect } from "vitest";
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
14
|
+
import { AuditEventTypeSchema, AuditActorTypeSchema } from "../types.js";
|
|
13
15
|
describe("audit_events immutability", () => {
|
|
14
16
|
it("only audit/retention.ts may delete or update audit_events rows", () => {
|
|
15
17
|
const repoRoot = path.resolve(__dirname, "../../../..");
|
|
@@ -56,8 +58,14 @@ describe("audit_events immutability", () => {
|
|
|
56
58
|
"packages/core/src/pm/actions/resolve-approvals.ts",
|
|
57
59
|
"packages/core/src/pm/actions/recover-stuck.ts",
|
|
58
60
|
"packages/core/src/pipeline/review-providers-runner.ts",
|
|
59
|
-
"packages/core/src/release-manager/
|
|
61
|
+
"packages/core/src/release-manager/release-tick.ts",
|
|
62
|
+
"packages/core/src/release-manager/release-helpers.ts",
|
|
60
63
|
"packages/core/src/release-manager/slack-handler.ts",
|
|
64
|
+
// BEC-207: base-tier operational signal — see auth-monitor.ts comment
|
|
65
|
+
// for the rationale on bypassing the audit-log feature gate.
|
|
66
|
+
"packages/core/src/executor/auth-monitor.ts",
|
|
67
|
+
"packages/core/src/__tests__/auth-monitor.test.ts",
|
|
68
|
+
"packages/core/src/audit/events.ts",
|
|
61
69
|
"packages/core/src/repo/agent-branch-sweep-runner.ts",
|
|
62
70
|
"packages/core/src/qa/github.ts",
|
|
63
71
|
"packages/core/src/qa/gap.ts",
|
|
@@ -76,5 +84,46 @@ describe("audit_events immutability", () => {
|
|
|
76
84
|
.filter((file) => !allowed.some((a) => file.endsWith(a) || file === a));
|
|
77
85
|
expect(offenders, `Unauthorized logAuditEventUnchecked usage in:\n${offenders.join("\n")}`).toEqual([]);
|
|
78
86
|
});
|
|
87
|
+
/**
|
|
88
|
+
* Tier 1d — keep CLAUDE.md's claimed audit-event count in sync with
|
|
89
|
+
* AuditEventTypeSchema. The autonomous pipeline has historically added new
|
|
90
|
+
* event types to the schema without updating the doc count (see Tier 1a's
|
|
91
|
+
* pre-existing 17→41 drift). This test fails CI when the regex match
|
|
92
|
+
* `(\d+) event types` in CLAUDE.md disagrees with `AuditEventTypeSchema`'s
|
|
93
|
+
* length.
|
|
94
|
+
*
|
|
95
|
+
* The pipeline's review-stage prompt will surface this as a blocking
|
|
96
|
+
* `audit-count-drift` finding via the convention checklist (Tier 2);
|
|
97
|
+
* the unit test is the deterministic backstop.
|
|
98
|
+
*/
|
|
99
|
+
it("CLAUDE.md audit-event count matches AuditEventTypeSchema length", () => {
|
|
100
|
+
const repoRoot = path.resolve(__dirname, "../../../..");
|
|
101
|
+
const claudeMd = readFileSync(path.join(repoRoot, "CLAUDE.md"), "utf8");
|
|
102
|
+
const matches = [...claudeMd.matchAll(/(\d+)\s+event\s+types/g)];
|
|
103
|
+
expect(matches.length, "CLAUDE.md must contain exactly one `<N> event types` sentence so Tier 1d can lock it down").toBe(1);
|
|
104
|
+
const documented = Number(matches[0][1]);
|
|
105
|
+
const actual = AuditEventTypeSchema.options.length;
|
|
106
|
+
expect(documented, `CLAUDE.md says "${documented} event types" but AuditEventTypeSchema has ${actual}. Update CLAUDE.md or the schema; they must match.`).toBe(actual);
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* Tier 1d — mirror of the event-type count check for the actor-type enum.
|
|
110
|
+
* The actor-type list is shorter and changes less often, but the same drift
|
|
111
|
+
* pattern applies (e.g. BEC-207 added a new actor without updating any doc).
|
|
112
|
+
* CLAUDE.md doesn't currently cite a number for actor types, so this test
|
|
113
|
+
* is gated: it only runs if a `(\d+) actor types` sentence is present, and
|
|
114
|
+
* fails when present-but-stale. Removes the test gracefully when the doc
|
|
115
|
+
* doesn't enumerate.
|
|
116
|
+
*/
|
|
117
|
+
it("CLAUDE.md actor-type count matches AuditActorTypeSchema length (when present)", () => {
|
|
118
|
+
const repoRoot = path.resolve(__dirname, "../../../..");
|
|
119
|
+
const claudeMd = readFileSync(path.join(repoRoot, "CLAUDE.md"), "utf8");
|
|
120
|
+
const matches = [...claudeMd.matchAll(/(\d+)\s+actor\s+types/g)];
|
|
121
|
+
if (matches.length === 0)
|
|
122
|
+
return; // not enumerated; nothing to validate
|
|
123
|
+
expect(matches.length, "CLAUDE.md must contain at most one `<N> actor types` sentence").toBe(1);
|
|
124
|
+
const documented = Number(matches[0][1]);
|
|
125
|
+
const actual = AuditActorTypeSchema.options.length;
|
|
126
|
+
expect(documented, `CLAUDE.md says "${documented} actor types" but AuditActorTypeSchema has ${actual}.`).toBe(actual);
|
|
127
|
+
});
|
|
79
128
|
});
|
|
80
129
|
//# sourceMappingURL=audit-immutability.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-immutability.test.js","sourceRoot":"","sources":["../../src/__tests__/audit-immutability.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"audit-immutability.test.js","sourceRoot":"","sources":["../../src/__tests__/audit-immutability.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-monitor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth-monitor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AuthMonitor (BEC-207) — periodic Claude session health-check.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Returns early when CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY is set.
|
|
6
|
+
* - Runs `claude auth status` only when neither env var is set.
|
|
7
|
+
* - On expiry: sends Slack alert and writes claude.auth_expired audit event.
|
|
8
|
+
* - 6-hour throttle: skips check if interval has not elapsed.
|
|
9
|
+
* - createAuthMonitor stateful wrapper manages lastCheckTime across calls.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mocks — must be declared before any imports that use the mocked modules.
|
|
14
|
+
// vi.mock() is hoisted by vitest, so these run before module initialisation.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Mock child_process.execFile used by runAuthMonitorCheck
|
|
17
|
+
vi.mock("node:child_process", () => ({
|
|
18
|
+
execFile: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
// Mock postSlackMessage
|
|
21
|
+
vi.mock("../pm/slack-helpers.js", () => ({
|
|
22
|
+
postSlackMessage: vi.fn().mockResolvedValue({ ok: true }),
|
|
23
|
+
}));
|
|
24
|
+
// Mock audit writer
|
|
25
|
+
vi.mock("../audit/writer.js", () => ({
|
|
26
|
+
logAuditEventUnchecked: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
}));
|
|
28
|
+
// Mock resetAuthCheckCache (auth-monitor calls this to bypass the 5-min cache)
|
|
29
|
+
vi.mock("../executor/auth-check.js", () => ({
|
|
30
|
+
resetAuthCheckCache: vi.fn(),
|
|
31
|
+
resolveClaudeAuth: vi.fn(() => ({ method: "session" })),
|
|
32
|
+
isClaudeAuthValid: vi.fn().mockResolvedValue(true),
|
|
33
|
+
}));
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Static imports (mocks are in place due to hoisting)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
import { execFile } from "node:child_process";
|
|
38
|
+
import { postSlackMessage } from "../pm/slack-helpers.js";
|
|
39
|
+
import { logAuditEventUnchecked } from "../audit/writer.js";
|
|
40
|
+
import { runAuthMonitorCheck, createAuthMonitor } from "../executor/auth-monitor.js";
|
|
41
|
+
const mockExecFile = vi.mocked(execFile);
|
|
42
|
+
const mockPostSlackMessage = vi.mocked(postSlackMessage);
|
|
43
|
+
const mockLogAuditEvent = vi.mocked(logAuditEventUnchecked);
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Helpers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
function simulateCliSuccess() {
|
|
48
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
49
|
+
cb(null);
|
|
50
|
+
return {};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function simulateCliFailure() {
|
|
54
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
55
|
+
cb(new Error("session expired"));
|
|
56
|
+
return {};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const SMALL_INTERVAL = 100; // 100 ms — always elapsed unless we use a very recent timestamp
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Tests: runAuthMonitorCheck
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
describe("runAuthMonitorCheck", () => {
|
|
64
|
+
let savedOauthToken;
|
|
65
|
+
let savedApiKey;
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
mockExecFile.mockReset();
|
|
68
|
+
mockPostSlackMessage.mockReset().mockResolvedValue({ ok: true });
|
|
69
|
+
mockLogAuditEvent.mockReset().mockResolvedValue(undefined);
|
|
70
|
+
// Save and clear env vars so each test starts with a clean slate
|
|
71
|
+
savedOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
72
|
+
savedApiKey = process.env.ANTHROPIC_API_KEY;
|
|
73
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
74
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
75
|
+
});
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
if (savedOauthToken === undefined) {
|
|
78
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = savedOauthToken;
|
|
82
|
+
}
|
|
83
|
+
if (savedApiKey === undefined) {
|
|
84
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
process.env.ANTHROPIC_API_KEY = savedApiKey;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// --- Throttle ---
|
|
91
|
+
it("skips check when interval has not elapsed (lastCheckTime is recent)", async () => {
|
|
92
|
+
const recentTime = Date.now(); // just ran
|
|
93
|
+
await runAuthMonitorCheck(recentTime, {}, 60_000); // 60-second interval
|
|
94
|
+
// Should not run execFile because interval hasn't elapsed
|
|
95
|
+
expect(mockExecFile).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it("runs check when interval has elapsed (lastCheckTime = 0 = never)", async () => {
|
|
98
|
+
simulateCliSuccess();
|
|
99
|
+
await runAuthMonitorCheck(0, {}, SMALL_INTERVAL);
|
|
100
|
+
expect(mockExecFile).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
// --- Env-var short-circuit (AC #8) ---
|
|
103
|
+
it("returns early without subprocess when CLAUDE_CODE_OAUTH_TOKEN is set", async () => {
|
|
104
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat-test";
|
|
105
|
+
await runAuthMonitorCheck(0, {}, SMALL_INTERVAL);
|
|
106
|
+
expect(mockExecFile).not.toHaveBeenCalled();
|
|
107
|
+
expect(mockPostSlackMessage).not.toHaveBeenCalled();
|
|
108
|
+
expect(mockLogAuditEvent).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
it("returns early without subprocess when ANTHROPIC_API_KEY is set", async () => {
|
|
111
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-test";
|
|
112
|
+
await runAuthMonitorCheck(0, {}, SMALL_INTERVAL);
|
|
113
|
+
expect(mockExecFile).not.toHaveBeenCalled();
|
|
114
|
+
expect(mockPostSlackMessage).not.toHaveBeenCalled();
|
|
115
|
+
expect(mockLogAuditEvent).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
it("returns early without subprocess when both env vars are set", async () => {
|
|
118
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat-test";
|
|
119
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-test";
|
|
120
|
+
await runAuthMonitorCheck(0, {}, SMALL_INTERVAL);
|
|
121
|
+
expect(mockExecFile).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
// --- Session valid ---
|
|
124
|
+
it("runs subprocess and skips alerts when session is valid", async () => {
|
|
125
|
+
simulateCliSuccess();
|
|
126
|
+
await runAuthMonitorCheck(0, {
|
|
127
|
+
slackBotToken: "xoxb-test",
|
|
128
|
+
slackErrorChannel: "CTEST",
|
|
129
|
+
}, SMALL_INTERVAL);
|
|
130
|
+
expect(mockExecFile).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(mockPostSlackMessage).not.toHaveBeenCalled();
|
|
132
|
+
expect(mockLogAuditEvent).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
// --- Session expired (AC #7, #15) ---
|
|
135
|
+
it("sends Slack alert to configured channel when session is expired", async () => {
|
|
136
|
+
simulateCliFailure();
|
|
137
|
+
await runAuthMonitorCheck(0, {
|
|
138
|
+
slackBotToken: "xoxb-bot-token",
|
|
139
|
+
slackErrorChannel: "CERROR",
|
|
140
|
+
}, SMALL_INTERVAL);
|
|
141
|
+
expect(mockPostSlackMessage).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(mockPostSlackMessage).toHaveBeenCalledWith("xoxb-bot-token", expect.objectContaining({ channel: "CERROR" }));
|
|
143
|
+
});
|
|
144
|
+
it("logs claude.auth_expired audit event when session is expired and db provided (AC #7)", async () => {
|
|
145
|
+
simulateCliFailure();
|
|
146
|
+
const fakeDb = {};
|
|
147
|
+
await runAuthMonitorCheck(0, { db: fakeDb }, SMALL_INTERVAL);
|
|
148
|
+
expect(mockLogAuditEvent).toHaveBeenCalledTimes(1);
|
|
149
|
+
const [calledDb, calledEvent] = mockLogAuditEvent.mock.calls[0];
|
|
150
|
+
expect(calledDb).toBe(fakeDb);
|
|
151
|
+
expect(calledEvent.eventType).toBe("claude.auth_expired");
|
|
152
|
+
expect(calledEvent.actor).toBe("system");
|
|
153
|
+
expect(calledEvent.payload).toMatchObject({
|
|
154
|
+
detectedAt: expect.any(String),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it("does not throw when Slack post fails", async () => {
|
|
158
|
+
simulateCliFailure();
|
|
159
|
+
mockPostSlackMessage.mockRejectedValue(new Error("network error"));
|
|
160
|
+
await expect(runAuthMonitorCheck(0, {
|
|
161
|
+
slackBotToken: "xoxb-test",
|
|
162
|
+
slackErrorChannel: "CTEST",
|
|
163
|
+
}, SMALL_INTERVAL)).resolves.not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
it("skips Slack when no slackBotToken configured", async () => {
|
|
166
|
+
simulateCliFailure();
|
|
167
|
+
await runAuthMonitorCheck(0, { slackErrorChannel: "CTEST" }, SMALL_INTERVAL);
|
|
168
|
+
expect(mockPostSlackMessage).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
it("skips Slack when no slackErrorChannel configured", async () => {
|
|
171
|
+
simulateCliFailure();
|
|
172
|
+
await runAuthMonitorCheck(0, { slackBotToken: "xoxb-test" }, SMALL_INTERVAL);
|
|
173
|
+
expect(mockPostSlackMessage).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
// --- Return value (lastCheckTime update) ---
|
|
176
|
+
it("returns updated timestamp after running", async () => {
|
|
177
|
+
simulateCliSuccess();
|
|
178
|
+
const before = Date.now();
|
|
179
|
+
const returned = await runAuthMonitorCheck(0, {}, SMALL_INTERVAL);
|
|
180
|
+
const after = Date.now();
|
|
181
|
+
expect(returned).toBeGreaterThanOrEqual(before);
|
|
182
|
+
expect(returned).toBeLessThanOrEqual(after);
|
|
183
|
+
});
|
|
184
|
+
it("returns original lastCheckTime when interval has not elapsed", async () => {
|
|
185
|
+
const recentTime = Date.now();
|
|
186
|
+
const returned = await runAuthMonitorCheck(recentTime, {}, 60_000);
|
|
187
|
+
expect(returned).toBe(recentTime);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Tests: createAuthMonitor stateful wrapper (AC #6)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
describe("createAuthMonitor", () => {
|
|
194
|
+
let savedOauthToken;
|
|
195
|
+
let savedApiKey;
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
mockExecFile.mockReset();
|
|
198
|
+
mockPostSlackMessage.mockReset().mockResolvedValue({ ok: true });
|
|
199
|
+
mockLogAuditEvent.mockReset().mockResolvedValue(undefined);
|
|
200
|
+
savedOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
201
|
+
savedApiKey = process.env.ANTHROPIC_API_KEY;
|
|
202
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
203
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
204
|
+
});
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
if (savedOauthToken === undefined) {
|
|
207
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = savedOauthToken;
|
|
211
|
+
}
|
|
212
|
+
if (savedApiKey === undefined) {
|
|
213
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
process.env.ANTHROPIC_API_KEY = savedApiKey;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
it("tick() runs the check and handles errors gracefully", async () => {
|
|
220
|
+
simulateCliSuccess();
|
|
221
|
+
const monitor = createAuthMonitor({}, SMALL_INTERVAL);
|
|
222
|
+
await expect(monitor.tick()).resolves.not.toThrow();
|
|
223
|
+
expect(mockExecFile).toHaveBeenCalledTimes(1);
|
|
224
|
+
});
|
|
225
|
+
it("tick() is throttled — second call immediately after first is a no-op", async () => {
|
|
226
|
+
simulateCliSuccess();
|
|
227
|
+
const monitor = createAuthMonitor({}, 60_000); // 60s interval
|
|
228
|
+
await monitor.tick(); // first tick — runs
|
|
229
|
+
await monitor.tick(); // second tick immediately — throttled
|
|
230
|
+
expect(mockExecFile).toHaveBeenCalledTimes(1); // only one subprocess
|
|
231
|
+
});
|
|
232
|
+
it("tick() skips session check when CLAUDE_CODE_OAUTH_TOKEN is set (AC #8)", async () => {
|
|
233
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat-test";
|
|
234
|
+
const monitor = createAuthMonitor({}, SMALL_INTERVAL);
|
|
235
|
+
await monitor.tick();
|
|
236
|
+
expect(mockExecFile).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
it("tick() sends alert on expired session and logs audit event (AC #6, #7)", async () => {
|
|
239
|
+
simulateCliFailure();
|
|
240
|
+
const fakeDb = {};
|
|
241
|
+
const monitor = createAuthMonitor({
|
|
242
|
+
slackBotToken: "xoxb-test",
|
|
243
|
+
slackErrorChannel: "CALERTS",
|
|
244
|
+
db: fakeDb,
|
|
245
|
+
}, SMALL_INTERVAL);
|
|
246
|
+
await monitor.tick();
|
|
247
|
+
expect(mockPostSlackMessage).toHaveBeenCalledWith("xoxb-test", expect.objectContaining({ channel: "CALERTS" }));
|
|
248
|
+
expect(mockLogAuditEvent).toHaveBeenCalledTimes(1);
|
|
249
|
+
const [, calledEvent] = mockLogAuditEvent.mock.calls[0];
|
|
250
|
+
expect(calledEvent.eventType).toBe("claude.auth_expired");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
//# sourceMappingURL=auth-monitor.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-monitor.test.js","sourceRoot":"","sources":["../../src/__tests__/auth-monitor.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEC-186: pull_request.closed + merged=true must transition Linear to Done.
|
|
3
|
+
*
|
|
4
|
+
* When a human (or GitHub's "auto-merge when ready") merges a PR after the
|
|
5
|
+
* pipeline has already completed, the pipeline's `onPipelineComplete` has
|
|
6
|
+
* already fired and will not re-fire. Without an explicit handler for
|
|
7
|
+
* `pull_request.closed+merged`, the Linear ticket stays "In Review" forever.
|
|
8
|
+
*
|
|
9
|
+
* Fix implemented in `webhook/github-handler.ts`:
|
|
10
|
+
* - A new routing branch catches `pull_request.closed` + `merged: true`
|
|
11
|
+
* - Looks up the pipeline run by pr_url
|
|
12
|
+
* - Marks `auto_merged = true` in the DB
|
|
13
|
+
* - Calls `notifier.onPRMerged?.(run)` → LinearNotifier transitions to Done
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=bec-186-repro.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bec-186-repro.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bec-186-repro.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEC-186: pull_request.closed + merged=true must transition Linear to Done.
|
|
3
|
+
*
|
|
4
|
+
* When a human (or GitHub's "auto-merge when ready") merges a PR after the
|
|
5
|
+
* pipeline has already completed, the pipeline's `onPipelineComplete` has
|
|
6
|
+
* already fired and will not re-fire. Without an explicit handler for
|
|
7
|
+
* `pull_request.closed+merged`, the Linear ticket stays "In Review" forever.
|
|
8
|
+
*
|
|
9
|
+
* Fix implemented in `webhook/github-handler.ts`:
|
|
10
|
+
* - A new routing branch catches `pull_request.closed` + `merged: true`
|
|
11
|
+
* - Looks up the pipeline run by pr_url
|
|
12
|
+
* - Marks `auto_merged = true` in the DB
|
|
13
|
+
* - Calls `notifier.onPRMerged?.(run)` → LinearNotifier transitions to Done
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
16
|
+
import { createHmac } from "crypto";
|
|
17
|
+
import { createGitHubWebhookHandler } from "../webhook/github-handler.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers (mirrors existing test infrastructure)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const SECRET = "gh-webhook-secret";
|
|
22
|
+
function sign(body, secret) {
|
|
23
|
+
return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
const pipelineConfig = {
|
|
26
|
+
name: "auto-implement",
|
|
27
|
+
stages: ["triage", "implement", "test", "review"],
|
|
28
|
+
retry: { maxAttempts: 1, strategy: "fix-and-retry" },
|
|
29
|
+
review: { requiredApprovals: 1 },
|
|
30
|
+
prStrategy: "draft",
|
|
31
|
+
};
|
|
32
|
+
const repoConfig = {
|
|
33
|
+
url: "https://github.com/org/repo",
|
|
34
|
+
defaultBranch: "main",
|
|
35
|
+
testCommand: "npm test",
|
|
36
|
+
buildCommand: "npm run build",
|
|
37
|
+
githubFeedback: {
|
|
38
|
+
allowedReviewers: [],
|
|
39
|
+
botLogins: ["linear-agent[bot]"],
|
|
40
|
+
autoTrigger: true,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/** Simulated DB row representing a completed pipeline run whose PR was opened. */
|
|
44
|
+
const mockRun = {
|
|
45
|
+
id: "run-bec186",
|
|
46
|
+
issueId: "BEC-179",
|
|
47
|
+
issueTitle: "Worktree detached HEAD bug",
|
|
48
|
+
pipelineKey: "auto-implement",
|
|
49
|
+
repoUrl: "https://github.com/org/repo",
|
|
50
|
+
branch: "agent/BEC-179-worktree-detached-head",
|
|
51
|
+
status: "completed",
|
|
52
|
+
prUrl: "https://github.com/org/repo/pull/235",
|
|
53
|
+
autoMerged: null,
|
|
54
|
+
runType: "standard",
|
|
55
|
+
parentRunId: null,
|
|
56
|
+
feedbackContext: null,
|
|
57
|
+
};
|
|
58
|
+
function makeMockDb(run = mockRun) {
|
|
59
|
+
const updateMock = vi.fn().mockReturnValue({
|
|
60
|
+
set: vi.fn().mockReturnValue({
|
|
61
|
+
where: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
select: vi.fn().mockReturnValue({
|
|
66
|
+
from: vi.fn().mockReturnValue({
|
|
67
|
+
where: vi.fn().mockReturnValue({
|
|
68
|
+
limit: vi.fn().mockResolvedValue(run ? [run] : []),
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
}),
|
|
72
|
+
update: updateMock,
|
|
73
|
+
_updateMock: updateMock,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function makeMockRunner() {
|
|
77
|
+
return {
|
|
78
|
+
startFeedback: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
isActiveFeedback: vi.fn().mockReturnValue(false),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function makeMockNotifier() {
|
|
83
|
+
return {
|
|
84
|
+
onPipelineStart: vi.fn().mockResolvedValue(undefined),
|
|
85
|
+
onStageComplete: vi.fn().mockResolvedValue(undefined),
|
|
86
|
+
onPipelineComplete: vi.fn().mockResolvedValue(undefined),
|
|
87
|
+
onPipelineFailed: vi.fn().mockResolvedValue(undefined),
|
|
88
|
+
onPRMerged: vi.fn().mockResolvedValue(undefined),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function buildConfig(overrides = {}) {
|
|
92
|
+
return {
|
|
93
|
+
webhookSecret: SECRET,
|
|
94
|
+
runner: makeMockRunner(),
|
|
95
|
+
pipelineConfigs: { "auto-implement": pipelineConfig },
|
|
96
|
+
repoConfigs: { "org/repo": repoConfig },
|
|
97
|
+
db: makeMockDb(),
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Creates a synthetic pull_request.closed+merged webhook payload. */
|
|
102
|
+
function makePRClosedMergedPayload(overrides = {}) {
|
|
103
|
+
return {
|
|
104
|
+
action: "closed",
|
|
105
|
+
pull_request: {
|
|
106
|
+
number: 235,
|
|
107
|
+
html_url: "https://github.com/org/repo/pull/235",
|
|
108
|
+
merged: true,
|
|
109
|
+
head: { ref: "agent/BEC-179-worktree-detached-head" },
|
|
110
|
+
merge_commit_sha: "abc123def456",
|
|
111
|
+
},
|
|
112
|
+
repository: {
|
|
113
|
+
name: "repo",
|
|
114
|
+
owner: { login: "org" },
|
|
115
|
+
html_url: "https://github.com/org/repo",
|
|
116
|
+
},
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function postWebhook(app, body, event, secret = SECRET) {
|
|
121
|
+
const rawBody = JSON.stringify(body);
|
|
122
|
+
const sig = sign(rawBody, secret);
|
|
123
|
+
return app.request("/webhooks/github", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"X-GitHub-Event": event,
|
|
128
|
+
"X-Hub-Signature-256": sig,
|
|
129
|
+
},
|
|
130
|
+
body: rawBody,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// BEC-186 tests — verifying the fix
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
describe("BEC-186: pull_request.closed + merged=true → Linear Done transition", () => {
|
|
137
|
+
let runner;
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
vi.restoreAllMocks();
|
|
140
|
+
runner = makeMockRunner();
|
|
141
|
+
});
|
|
142
|
+
it("pull_request.closed+merged=true is handled (not skipped)", async () => {
|
|
143
|
+
// The core fix: action="closed"+merged=true must NOT fall through to
|
|
144
|
+
// the "unhandled event type" branch.
|
|
145
|
+
const db = makeMockDb(mockRun);
|
|
146
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db }));
|
|
147
|
+
const res = await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
148
|
+
expect(res.status).toBe(200);
|
|
149
|
+
const json = await res.json();
|
|
150
|
+
expect(json).not.toHaveProperty("skipped");
|
|
151
|
+
expect(json.ok).toBe(true);
|
|
152
|
+
expect(json.action).toBe("pr-merged");
|
|
153
|
+
});
|
|
154
|
+
it("DB is updated with auto_merged=true on PR merge", async () => {
|
|
155
|
+
const db = makeMockDb(mockRun);
|
|
156
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db }));
|
|
157
|
+
await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
158
|
+
expect(db._updateMock).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
it("notifier.onPRMerged is called when notifier is provided", async () => {
|
|
161
|
+
const db = makeMockDb(mockRun);
|
|
162
|
+
const notifier = makeMockNotifier();
|
|
163
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db, notifier: notifier }));
|
|
164
|
+
await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
165
|
+
expect(notifier.onPRMerged).toHaveBeenCalledOnce();
|
|
166
|
+
expect(notifier.onPRMerged).toHaveBeenCalledWith(expect.objectContaining({ id: "run-bec186" }));
|
|
167
|
+
});
|
|
168
|
+
it("notifier.onPRMerged is NOT called when no pipeline run is found", async () => {
|
|
169
|
+
// PR from a non-agent source — no DB row matches
|
|
170
|
+
const db = makeMockDb(null);
|
|
171
|
+
const notifier = makeMockNotifier();
|
|
172
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db, notifier: notifier }));
|
|
173
|
+
const res = await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
174
|
+
expect(res.status).toBe(200);
|
|
175
|
+
const json = await res.json();
|
|
176
|
+
// Still returns ok: true, action: "pr-merged" (handler ran, just found no run)
|
|
177
|
+
expect(json.ok).toBe(true);
|
|
178
|
+
expect(notifier.onPRMerged).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
it("idempotency: already-merged run is not re-processed", async () => {
|
|
181
|
+
// If auto_merged is already true, skip the update and notifier call
|
|
182
|
+
const alreadyMergedRun = { ...mockRun, autoMerged: true };
|
|
183
|
+
const db = makeMockDb(alreadyMergedRun);
|
|
184
|
+
const notifier = makeMockNotifier();
|
|
185
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db, notifier: notifier }));
|
|
186
|
+
await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
187
|
+
expect(db._updateMock).not.toHaveBeenCalled();
|
|
188
|
+
expect(notifier.onPRMerged).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
it("pull_request.closed WITHOUT merged=true is skipped (no Done transition)", async () => {
|
|
191
|
+
// PR closed without merge (e.g. closed manually) must NOT trigger Done transition.
|
|
192
|
+
const db = makeMockDb(mockRun);
|
|
193
|
+
const notifier = makeMockNotifier();
|
|
194
|
+
const payload = {
|
|
195
|
+
...makePRClosedMergedPayload(),
|
|
196
|
+
pull_request: {
|
|
197
|
+
...makePRClosedMergedPayload().pull_request,
|
|
198
|
+
merged: false,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db, notifier: notifier }));
|
|
202
|
+
const res = await postWebhook(app, payload, "pull_request");
|
|
203
|
+
expect(res.status).toBe(200);
|
|
204
|
+
const json = await res.json();
|
|
205
|
+
// Falls through to "unhandled event type" — correct outcome
|
|
206
|
+
expect(json.skipped).toBeDefined();
|
|
207
|
+
expect(db._updateMock).not.toHaveBeenCalled();
|
|
208
|
+
expect(notifier.onPRMerged).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
it("works when notifier is not configured (no crash)", async () => {
|
|
211
|
+
// Notifier is optional — handler must not throw when absent
|
|
212
|
+
const db = makeMockDb(mockRun);
|
|
213
|
+
const app = createGitHubWebhookHandler(buildConfig({ runner: runner, db: db /* no notifier */ }));
|
|
214
|
+
const res = await postWebhook(app, makePRClosedMergedPayload(), "pull_request");
|
|
215
|
+
expect(res.status).toBe(200);
|
|
216
|
+
const json = await res.json();
|
|
217
|
+
expect(json.ok).toBe(true);
|
|
218
|
+
expect(json.action).toBe("pr-merged");
|
|
219
|
+
// DB still updated even without notifier
|
|
220
|
+
expect(db._updateMock).toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
//# sourceMappingURL=bec-186-repro.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bec-186-repro.test.js","sourceRoot":"","sources":["../../src/__tests__/bec-186-repro.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-migrations.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/db-migrations.test.ts"],"names":[],"mappings":""}
|