-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathdeploymentRecorder.test.js
More file actions
256 lines (229 loc) · 10.4 KB
/
Copy pathdeploymentRecorder.test.js
File metadata and controls
256 lines (229 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
'use strict';
// Unit tests for the per-peer tracking and row-await helpers in DeploymentRecorder:
// - `recordPeers()` — normalizes the opaque replication-layer per-peer outcomes into
// a stable `[{node, status, error?, started_at, completed_at}]` shape on the row.
// - `awaitDeploymentRow()` — peer-side helper that polls the hdb_deployment table
// until the row arrives via replication, then returns it.
//
// These exercise the table layer via a tiny mock attached to `databases.system` —
// the recorder's `put()` already tolerates a missing table, so we only mock when we
// need to control the `.get()` return value or assert side effects.
const assert = require('node:assert');
const testUtils = require('../testUtils.js');
testUtils.preTestPrep();
const { DeploymentRecorder, awaitDeploymentRow } = require('#src/components/deploymentRecorder');
const { databases } = require('#src/resources/databases');
const terms = require('#src/utility/hdbTerms');
const DEPLOYMENT_TABLE = terms.SYSTEM_TABLE_NAMES.DEPLOYMENT_TABLE_NAME;
// Lightweight mock: keeps a Map of rows, exposes get(id) and put(row).
function installMockDeploymentTable() {
const rows = new Map();
const mock = {
rows,
async get(id) {
return rows.get(id);
},
async put(row) {
rows.set(row.deployment_id, row);
},
};
if (!databases.system) databases.system = {};
const prior = databases.system[DEPLOYMENT_TABLE];
databases.system[DEPLOYMENT_TABLE] = mock;
return {
mock,
restore() {
databases.system[DEPLOYMENT_TABLE] = prior;
},
};
}
describe('DeploymentRecorder.recordPeer', () => {
let installed;
beforeEach(() => {
installed = installMockDeploymentTable();
});
afterEach(() => installed.restore());
it('normalizes a single-peer success result', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'node-b', status: 'success', started_at: 1000, completed_at: 1500 });
assert.deepStrictEqual(recorder.row.peer_results, [
{ node: 'node-b', status: 'success', error: null, started_at: 1000, completed_at: 1500 },
]);
});
it('maps an Error-bearing result to status="failed" with structured error', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'node-c', error: { message: 'install timed out', code: 'ETIMEDOUT' } });
assert.deepStrictEqual(recorder.row.peer_results, [
{
node: 'node-c',
status: 'failed',
error: { message: 'install timed out', code: 'ETIMEDOUT' },
started_at: null,
completed_at: null,
},
]);
});
it('treats a string-shaped error as failed and preserves the message', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'node-d', error: 'connection refused' });
assert.strictEqual(recorder.row.peer_results[0].status, 'failed');
assert.strictEqual(recorder.row.peer_results[0].error.message, 'connection refused');
});
it('falls back to "name"/"hostname" when "node" is missing', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ name: 'node-by-name', status: 'success' });
recorder.recordPeer({ hostname: 'node-by-hostname', status: 'success' });
assert.strictEqual(recorder.row.peer_results[0].node, 'node-by-name');
assert.strictEqual(recorder.row.peer_results[1].node, 'node-by-hostname');
});
it('preserves primitive entries as stringified raw markers', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer('node-x failed somehow');
assert.strictEqual(recorder.row.peer_results[0].status, 'unknown');
assert.strictEqual(recorder.row.peer_results[0].raw, 'node-x failed somehow');
});
it('upserts by node name on repeat calls — same peer transitions from pending to success', async () => {
// The real-time use case: first call recorder.recordPeer({node:'n1', status:'pending'}),
// then later recordPeer({node:'n1', status:'success'}). Only one entry should
// exist for 'n1', and it should reflect the latest call.
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'n1', status: 'pending' });
recorder.recordPeer({ node: 'n2', status: 'pending' });
assert.strictEqual(recorder.row.peer_results.length, 2);
recorder.recordPeer({ node: 'n1', status: 'success' });
assert.strictEqual(recorder.row.peer_results.length, 2, 'no duplicate entry for n1');
const n1 = recorder.row.peer_results.find((e) => e.node === 'n1');
assert.strictEqual(n1.status, 'success', 'n1 upserted to latest status');
});
it('is a no-op when called after finish()', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
await recorder.finish('success');
recorder.recordPeer({ node: 'node-late', status: 'success' });
assert.deepStrictEqual(recorder.row.peer_results, []);
});
it('persists via scheduleFlush so the row eventually reflects peer outcomes', async () => {
// recordPeer mutates in-memory and triggers the coalesced flush — by the time
// finish() drains pending puts, the persisted row carries peer_results.
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'node-z', status: 'success' });
await recorder.finish('success');
const persisted = await installed.mock.get(recorder.deploymentId);
assert.strictEqual(persisted.peer_results[0].node, 'node-z');
assert.strictEqual(persisted.peer_results[0].status, 'success');
assert.strictEqual(persisted.status, 'success');
});
});
describe('DeploymentRecorder.recordPeers (bulk wrapper)', () => {
let installed;
beforeEach(() => {
installed = installMockDeploymentTable();
});
afterEach(() => installed.restore());
it('iterates the input array and upserts each entry', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeers([
{ node: 'a', status: 'success' },
{ node: 'b', status: 'success' },
]);
assert.strictEqual(recorder.row.peer_results.length, 2);
});
it('is idempotent when the same entries arrive via per-peer and aggregate paths', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeer({ node: 'a', status: 'success' });
recorder.recordPeers([{ node: 'a', status: 'success' }]);
assert.strictEqual(recorder.row.peer_results.length, 1);
});
it('is a no-op for non-array inputs (defensive against odd replication shapes)', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
recorder.recordPeers(undefined);
recorder.recordPeers(null);
recorder.recordPeers('not an array');
recorder.recordPeers({ node: 'object-not-array' });
assert.deepStrictEqual(recorder.row.peer_results, []);
});
});
describe('DeploymentRecorder.seal', () => {
let installed;
let putLog;
beforeEach(() => {
putLog = [];
const rows = new Map();
const mock = {
rows,
async get(id) {
return rows.get(id);
},
async put(row) {
putLog.push({ status: row.status, peerCount: (row.peer_results ?? []).length });
rows.set(row.deployment_id, { ...row, peer_results: [...(row.peer_results ?? [])] });
},
};
if (!databases.system) databases.system = {};
const prior = databases.system[DEPLOYMENT_TABLE];
databases.system[DEPLOYMENT_TABLE] = mock;
installed = {
mock,
restore() {
databases.system[DEPLOYMENT_TABLE] = prior;
},
};
});
afterEach(() => installed.restore());
it('stops persisting intermediate updates once sealed, but finish() writes the terminal state', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
const putsAfterCreate = putLog.length;
recorder.seal();
recorder.recordPeer({ node: 'a', status: 'success' });
recorder.recordPeer({ node: 'b', status: 'success' });
assert.strictEqual(recorder.row.peer_results.length, 2, 'peer_results accumulate in memory while sealed');
assert.strictEqual(putLog.length, putsAfterCreate, 'no puts are issued while sealed (pre-finish)');
await recorder.finish('success');
const terminal = putLog[putLog.length - 1];
assert.strictEqual(terminal.status, 'success', 'finish() persists the terminal status');
assert.strictEqual(terminal.peerCount, 2, 'finish() carries the accumulated peer_results');
const persisted = await installed.mock.get(recorder.deploymentId);
assert.strictEqual(persisted.status, 'success');
assert.strictEqual(persisted.peer_results.length, 2);
});
it('does not affect persistence before seal: recordPeer still flushes incrementally', async () => {
const recorder = await DeploymentRecorder.create({ project: 'p' });
const putsAfterCreate = putLog.length;
recorder.recordPeer({ node: 'a', status: 'success' });
// scheduleFlush issues the put asynchronously; let it settle.
await new Promise((resolve) => setImmediate(resolve));
assert.ok(putLog.length > putsAfterCreate, 'an unsealed recordPeer persists incrementally');
});
});
describe('awaitDeploymentRow', () => {
let installed;
beforeEach(() => {
installed = installMockDeploymentTable();
});
afterEach(() => installed.restore());
it('returns the row immediately when it is already present with payload_blob', async () => {
const row = { deployment_id: 'd1', payload_blob: { fake: true } };
installed.mock.rows.set('d1', row);
const result = await awaitDeploymentRow('d1');
assert.strictEqual(result, row);
});
it('skips a row with no payload_blob (still in flight) and resolves once it arrives', async () => {
const id = 'd2';
installed.mock.rows.set(id, { deployment_id: id, payload_blob: null });
// Schedule a delayed write of the blob so the polling loop sees it.
setTimeout(() => {
installed.mock.rows.set(id, { deployment_id: id, payload_blob: { fake: true } });
}, 50);
const result = await awaitDeploymentRow(id, { timeoutMs: 1000, pollIntervalMs: 25 });
assert.ok(result.payload_blob);
});
it('rejects with a timeout error when the row never arrives within timeoutMs', async () => {
await assert.rejects(
() => awaitDeploymentRow('never-arrives', { timeoutMs: 100, pollIntervalMs: 25 }),
/Timed out after 100ms waiting for hdb_deployment row 'never-arrives'/
);
});
it('throws if the deployment table is missing entirely (not yet provisioned)', async () => {
delete databases.system[DEPLOYMENT_TABLE];
await assert.rejects(() => awaitDeploymentRow('d3'), /Deployment tracking is not initialized on this node/);
});
});