@wopr-network/platform-core 1.14.5 → 1.14.7
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/api/routes/admin-onboarding.js +3 -1
- package/dist/dht/bootstrap-manager.js +2 -1
- package/dist/dht/bootstrap-manager.test.js +10 -0
- package/dist/monetization/incident/postmortem.js +8 -2
- package/dist/monetization/incident/postmortem.test.js +35 -0
- package/package.json +1 -1
- package/src/api/routes/admin-onboarding.ts +3 -1
- package/src/dht/bootstrap-manager.test.ts +16 -0
- package/src/dht/bootstrap-manager.ts +2 -1
- package/src/monetization/incident/postmortem.test.ts +47 -0
- package/src/monetization/incident/postmortem.ts +9 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { logger } from "../../config/logger.js";
|
|
2
3
|
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
3
4
|
export function createAdminOnboardingRoutes(getRepo, auditLogger) {
|
|
4
5
|
const routes = new Hono();
|
|
@@ -23,7 +24,8 @@ export function createAdminOnboardingRoutes(getRepo, auditLogger) {
|
|
|
23
24
|
try {
|
|
24
25
|
body = (await c.req.json());
|
|
25
26
|
}
|
|
26
|
-
catch {
|
|
27
|
+
catch (err) {
|
|
28
|
+
logger.debug("Failed to parse onboarding script JSON body", { error: err });
|
|
27
29
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
28
30
|
}
|
|
29
31
|
const content = body.content;
|
|
@@ -215,7 +215,8 @@ export class DhtBootstrapManager {
|
|
|
215
215
|
const volume = this.docker.getVolume(name);
|
|
216
216
|
await volume.inspect();
|
|
217
217
|
}
|
|
218
|
-
catch (
|
|
218
|
+
catch (inspectErr) {
|
|
219
|
+
logger.debug(`Volume inspect failed for ${name}, will attempt to create`, { error: inspectErr });
|
|
219
220
|
try {
|
|
220
221
|
await this.docker.createVolume({ Name: name });
|
|
221
222
|
logger.info(`Created DHT state volume ${name}`);
|
|
@@ -93,6 +93,16 @@ describe("DhtBootstrapManager", () => {
|
|
|
93
93
|
await manager.ensureNode(0);
|
|
94
94
|
expect(docker.createVolume).toHaveBeenCalledWith({ Name: `${DHT_VOLUME_PREFIX}0` });
|
|
95
95
|
});
|
|
96
|
+
it("logs at debug level when volume inspect fails", async () => {
|
|
97
|
+
const { logger } = await import("../config/logger.js");
|
|
98
|
+
const debugSpy = vi.spyOn(logger, "debug");
|
|
99
|
+
docker._volume.inspect.mockRejectedValue(new Error("no such volume"));
|
|
100
|
+
const container = mockContainer("c-0", `${DHT_CONTAINER_PREFIX}0`);
|
|
101
|
+
docker.createContainer.mockResolvedValue(container);
|
|
102
|
+
await manager.ensureNode(0);
|
|
103
|
+
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining("Volume inspect failed"), expect.objectContaining({ error: expect.any(Error) }));
|
|
104
|
+
debugSpy.mockRestore();
|
|
105
|
+
});
|
|
96
106
|
it("passes DHT_PEERS env excluding self", async () => {
|
|
97
107
|
const container = mockContainer("c-1", `${DHT_CONTAINER_PREFIX}1`);
|
|
98
108
|
docker.createContainer.mockResolvedValue(container);
|
|
@@ -41,8 +41,8 @@ _[PENDING: Describe what fixed the issue and how service was restored.]_`,
|
|
|
41
41
|
actionItems: `| Action | Owner | Due Date | Priority |
|
|
42
42
|
|--------|-------|----------|----------|
|
|
43
43
|
| _[PENDING: Add action item]_ | _[PENDING: Owner]_ | _[PENDING: Date]_ | P1 |
|
|
44
|
-
| Improve detection alert thresholds | on-call-engineer |
|
|
45
|
-
| Add runbook link to alert notification | on-call-engineer |
|
|
44
|
+
| Improve detection alert thresholds | on-call-engineer | ${actionItemDueDate(incident)} | P2 |
|
|
45
|
+
| Add runbook link to alert notification | on-call-engineer | ${actionItemDueDate(incident)} | P2 |`,
|
|
46
46
|
lessonsLearned: `**What went well:**
|
|
47
47
|
- _[PENDING: What worked well in detection and response?]_
|
|
48
48
|
|
|
@@ -124,3 +124,9 @@ function formatDuration(ms) {
|
|
|
124
124
|
return `${hours}h`;
|
|
125
125
|
return `${hours}h ${remainingMinutes}m`;
|
|
126
126
|
}
|
|
127
|
+
function actionItemDueDate(incident) {
|
|
128
|
+
const base = incident.resolvedAt ?? incident.startedAt;
|
|
129
|
+
const offsetDays = incident.severity === "SEV1" ? 7 : 14;
|
|
130
|
+
const due = new Date(base.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
|
131
|
+
return due.toISOString().split("T")[0];
|
|
132
|
+
}
|
|
@@ -101,4 +101,39 @@ describe("generatePostMortemTemplate", () => {
|
|
|
101
101
|
expect(report.sections.actionItems).not.toContain("TODO");
|
|
102
102
|
expect(report.sections.lessonsLearned).not.toContain("TODO");
|
|
103
103
|
});
|
|
104
|
+
it("action items derive due dates from resolvedAt for resolved incidents", () => {
|
|
105
|
+
const report = generatePostMortemTemplate(makeIncident({
|
|
106
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
107
|
+
severity: "SEV1",
|
|
108
|
+
}));
|
|
109
|
+
// SEV1: 7-day offset from resolvedAt → 2026-01-22
|
|
110
|
+
expect(report.sections.actionItems).toContain("2026-01-22");
|
|
111
|
+
expect(report.sections.actionItems).not.toContain("TBD");
|
|
112
|
+
});
|
|
113
|
+
it("action items derive due dates from startedAt for ongoing incidents", () => {
|
|
114
|
+
const report = generatePostMortemTemplate(makeIncident({
|
|
115
|
+
resolvedAt: null,
|
|
116
|
+
startedAt: new Date("2026-01-15T12:00:00Z"),
|
|
117
|
+
severity: "SEV1",
|
|
118
|
+
}));
|
|
119
|
+
// SEV1: 7-day offset from startedAt → 2026-01-22
|
|
120
|
+
expect(report.sections.actionItems).toContain("2026-01-22");
|
|
121
|
+
expect(report.sections.actionItems).not.toContain("TBD");
|
|
122
|
+
});
|
|
123
|
+
it("SEV2 action items use 14-day offset", () => {
|
|
124
|
+
const report = generatePostMortemTemplate(makeIncident({
|
|
125
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
126
|
+
severity: "SEV2",
|
|
127
|
+
}));
|
|
128
|
+
// SEV2: 14-day offset from resolvedAt → 2026-01-29
|
|
129
|
+
expect(report.sections.actionItems).toContain("2026-01-29");
|
|
130
|
+
});
|
|
131
|
+
it("SEV3 action items use 14-day offset", () => {
|
|
132
|
+
const report = generatePostMortemTemplate(makeIncident({
|
|
133
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
134
|
+
severity: "SEV3",
|
|
135
|
+
}));
|
|
136
|
+
// SEV3: 14-day offset from resolvedAt → 2026-01-29
|
|
137
|
+
expect(report.sections.actionItems).toContain("2026-01-29");
|
|
138
|
+
});
|
|
104
139
|
});
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import { logger } from "../../config/logger.js";
|
|
3
4
|
import type { IOnboardingScriptRepository } from "../../onboarding/drizzle-onboarding-script-repository.js";
|
|
4
5
|
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
5
6
|
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
@@ -31,7 +32,8 @@ export function createAdminOnboardingRoutes(getRepo: RepoFactory, auditLogger?:
|
|
|
31
32
|
let body: Record<string, unknown>;
|
|
32
33
|
try {
|
|
33
34
|
body = (await c.req.json()) as Record<string, unknown>;
|
|
34
|
-
} catch {
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
logger.debug("Failed to parse onboarding script JSON body", { error: err });
|
|
35
37
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -115,6 +115,22 @@ describe("DhtBootstrapManager", () => {
|
|
|
115
115
|
expect(docker.createVolume).toHaveBeenCalledWith({ Name: `${DHT_VOLUME_PREFIX}0` });
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
it("logs at debug level when volume inspect fails", async () => {
|
|
119
|
+
const { logger } = await import("../config/logger.js");
|
|
120
|
+
const debugSpy = vi.spyOn(logger, "debug");
|
|
121
|
+
docker._volume.inspect.mockRejectedValue(new Error("no such volume"));
|
|
122
|
+
const container = mockContainer("c-0", `${DHT_CONTAINER_PREFIX}0`);
|
|
123
|
+
docker.createContainer.mockResolvedValue(container);
|
|
124
|
+
|
|
125
|
+
await manager.ensureNode(0);
|
|
126
|
+
|
|
127
|
+
expect(debugSpy).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringContaining("Volume inspect failed"),
|
|
129
|
+
expect.objectContaining({ error: expect.any(Error) }),
|
|
130
|
+
);
|
|
131
|
+
debugSpy.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
|
|
118
134
|
it("passes DHT_PEERS env excluding self", async () => {
|
|
119
135
|
const container = mockContainer("c-1", `${DHT_CONTAINER_PREFIX}1`);
|
|
120
136
|
docker.createContainer.mockResolvedValue(container);
|
|
@@ -257,7 +257,8 @@ export class DhtBootstrapManager {
|
|
|
257
257
|
try {
|
|
258
258
|
const volume = this.docker.getVolume(name);
|
|
259
259
|
await volume.inspect();
|
|
260
|
-
} catch (
|
|
260
|
+
} catch (inspectErr: unknown) {
|
|
261
|
+
logger.debug(`Volume inspect failed for ${name}, will attempt to create`, { error: inspectErr });
|
|
261
262
|
try {
|
|
262
263
|
await this.docker.createVolume({ Name: name });
|
|
263
264
|
logger.info(`Created DHT state volume ${name}`);
|
|
@@ -121,4 +121,51 @@ describe("generatePostMortemTemplate", () => {
|
|
|
121
121
|
expect(report.sections.actionItems).not.toContain("TODO");
|
|
122
122
|
expect(report.sections.lessonsLearned).not.toContain("TODO");
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
it("action items derive due dates from resolvedAt for resolved incidents", () => {
|
|
126
|
+
const report = generatePostMortemTemplate(
|
|
127
|
+
makeIncident({
|
|
128
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
129
|
+
severity: "SEV1",
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
// SEV1: 7-day offset from resolvedAt → 2026-01-22
|
|
133
|
+
expect(report.sections.actionItems).toContain("2026-01-22");
|
|
134
|
+
expect(report.sections.actionItems).not.toContain("TBD");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("action items derive due dates from startedAt for ongoing incidents", () => {
|
|
138
|
+
const report = generatePostMortemTemplate(
|
|
139
|
+
makeIncident({
|
|
140
|
+
resolvedAt: null,
|
|
141
|
+
startedAt: new Date("2026-01-15T12:00:00Z"),
|
|
142
|
+
severity: "SEV1",
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
// SEV1: 7-day offset from startedAt → 2026-01-22
|
|
146
|
+
expect(report.sections.actionItems).toContain("2026-01-22");
|
|
147
|
+
expect(report.sections.actionItems).not.toContain("TBD");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("SEV2 action items use 14-day offset", () => {
|
|
151
|
+
const report = generatePostMortemTemplate(
|
|
152
|
+
makeIncident({
|
|
153
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
154
|
+
severity: "SEV2",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
// SEV2: 14-day offset from resolvedAt → 2026-01-29
|
|
158
|
+
expect(report.sections.actionItems).toContain("2026-01-29");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("SEV3 action items use 14-day offset", () => {
|
|
162
|
+
const report = generatePostMortemTemplate(
|
|
163
|
+
makeIncident({
|
|
164
|
+
resolvedAt: new Date("2026-01-15T13:00:00Z"),
|
|
165
|
+
severity: "SEV3",
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
// SEV3: 14-day offset from resolvedAt → 2026-01-29
|
|
169
|
+
expect(report.sections.actionItems).toContain("2026-01-29");
|
|
170
|
+
});
|
|
124
171
|
});
|
|
@@ -79,8 +79,8 @@ _[PENDING: Describe what fixed the issue and how service was restored.]_`,
|
|
|
79
79
|
actionItems: `| Action | Owner | Due Date | Priority |
|
|
80
80
|
|--------|-------|----------|----------|
|
|
81
81
|
| _[PENDING: Add action item]_ | _[PENDING: Owner]_ | _[PENDING: Date]_ | P1 |
|
|
82
|
-
| Improve detection alert thresholds | on-call-engineer |
|
|
83
|
-
| Add runbook link to alert notification | on-call-engineer |
|
|
82
|
+
| Improve detection alert thresholds | on-call-engineer | ${actionItemDueDate(incident)} | P2 |
|
|
83
|
+
| Add runbook link to alert notification | on-call-engineer | ${actionItemDueDate(incident)} | P2 |`,
|
|
84
84
|
|
|
85
85
|
lessonsLearned: `**What went well:**
|
|
86
86
|
- _[PENDING: What worked well in detection and response?]_
|
|
@@ -165,3 +165,10 @@ function formatDuration(ms: number): string {
|
|
|
165
165
|
if (remainingMinutes === 0) return `${hours}h`;
|
|
166
166
|
return `${hours}h ${remainingMinutes}m`;
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
function actionItemDueDate(incident: IncidentSummary): string {
|
|
170
|
+
const base = incident.resolvedAt ?? incident.startedAt;
|
|
171
|
+
const offsetDays = incident.severity === "SEV1" ? 7 : 14;
|
|
172
|
+
const due = new Date(base.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
|
173
|
+
return due.toISOString().split("T")[0];
|
|
174
|
+
}
|