job-application-agent 3.4.2 → 3.6.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/README.md +23 -0
- package/installer/src/cli.mjs +8 -1
- package/installer/src/installer.mjs +8 -0
- package/job-application-agent/SKILL.md +32 -2
- package/job-application-agent/capabilities.json +3 -1
- package/job-application-agent/references/ACCOUNTING.md +104 -0
- package/job-application-agent/references/AUTONOMY.md +2 -0
- package/job-application-agent/references/CLOUD_STATE.md +2 -0
- package/job-application-agent/references/FREE_AI.md +39 -0
- package/job-application-agent/references/OUTREACH.md +210 -0
- package/job-application-agent/references/RUNS.md +35 -0
- package/job-application-agent/references/SCHEMAS.md +12 -2
- package/job-application-agent/references/agent-box/README.md +77 -0
- package/job-application-agent/references/agent-box/novnc.service.example +16 -0
- package/job-application-agent/scripts/application-accounting.mjs +246 -0
- package/job-application-agent/scripts/ats/answer-inject.mjs +203 -0
- package/job-application-agent/scripts/ats/submit-adapters.mjs +194 -0
- package/job-application-agent/scripts/attention-questions.mjs +111 -0
- package/job-application-agent/scripts/attention-resume-submit.mjs +450 -0
- package/job-application-agent/scripts/attention-runner-poll.mjs +328 -0
- package/job-application-agent/scripts/captcha-vendor.mjs +328 -0
- package/job-application-agent/scripts/cloud-state-client.mjs +62 -8
- package/job-application-agent/scripts/job-application.mjs +243 -27
- package/job-application-agent/scripts/novnc-display-guard.mjs +300 -0
- package/job-application-agent/scripts/outreach-cli.mjs +95 -0
- package/job-application-agent/scripts/outreach-domain.mjs +287 -0
- package/job-application-agent/scripts/outreach-store.mjs +72 -0
- package/job-application-agent/scripts/session-binding.mjs +474 -0
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/accounting-cli.test.mjs +86 -0
- package/job-application-agent/tests/accounting-cloud-client.test.mjs +183 -0
- package/job-application-agent/tests/accounting-retry-cli.test.mjs +96 -0
- package/job-application-agent/tests/accounting-source-race.test.mjs +109 -0
- package/job-application-agent/tests/answer-inject-captcha.test.mjs +169 -0
- package/job-application-agent/tests/application-accounting.test.mjs +315 -0
- package/job-application-agent/tests/attention-resume-submit.test.mjs +119 -0
- package/job-application-agent/tests/attention-runner-poll.test.mjs +135 -0
- package/job-application-agent/tests/fixtures/outreach.mjs +22 -0
- package/job-application-agent/tests/job-application.test.mjs +5 -0
- package/job-application-agent/tests/novnc-display-guard.test.mjs +50 -0
- package/job-application-agent/tests/outreach-cli.test.mjs +73 -0
- package/job-application-agent/tests/outreach.test.mjs +178 -0
- package/job-application-agent/tests/privacy-audit.test.mjs +2 -0
- package/job-application-agent/tests/review-cadence.test.mjs +66 -0
- package/job-application-agent/tests/session-binding.test.mjs +102 -0
- package/job-application-agent/tests/skill-contract.test.mjs +25 -0
- package/job-application-agent/tests/workflow-state.test.mjs +30 -3
- package/package.json +6 -3
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { deliveryProjection, discoveryProjection } from '../scripts/application-accounting.mjs';
|
|
5
|
+
|
|
6
|
+
const occurredAt = '2026-09-13T10:00:00Z';
|
|
7
|
+
|
|
8
|
+
function application(overrides = {}) {
|
|
9
|
+
return { id: 'application-1', company: 'Example', role: 'Engineer', source: 'email', url: 'https://example.com/jobs/123', status: 'submitted', submittedAt: '2026-09-12T10:00:00Z', ...overrides };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function delivery(overrides = {}) {
|
|
13
|
+
return { version: 1, id: 'bounce-1', applicationId: 'application-1', attemptId: 'initial:application-1', type: 'delivery-failed', occurredAt, evidenceType: 'final-delivery-failure', evidence: 'Final recipient failure matched to the sent application message.', ...overrides };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function lead(overrides = {}) {
|
|
17
|
+
return { version: 1, type: 'lead-reviewed', id: 'lead-event-1', roundId: 'round-1', sourceId: 'direct', url: 'https://example.com/jobs/123', company: 'Example', role: 'Engineer', disposition: 'qualified', observedAt: occurredAt, evidence: 'Active employer posting assessed against candidate requirements.', ...overrides };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('email sends count once while their receipt remains unknown', () => {
|
|
21
|
+
const result = deliveryProjection([application()], []);
|
|
22
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
23
|
+
assert.equal(result.receiptUnknownEmailCount, 1);
|
|
24
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
25
|
+
assert.equal(result.applications[0].receiptUnknown, true);
|
|
26
|
+
assert.equal(result.applications[0].attempts.length, 1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('confirmed browser applications need no email acknowledgement', () => {
|
|
30
|
+
const result = deliveryProjection([application({ source: 'ashby' })], []);
|
|
31
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
32
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a matched final email failure removes the application from effective totals', () => {
|
|
36
|
+
const result = deliveryProjection([application()], [delivery()]);
|
|
37
|
+
assert.equal(result.effectiveSubmissionCount, 0);
|
|
38
|
+
assert.equal(result.failedDeliveryCount, 1);
|
|
39
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
40
|
+
assert.equal(result.applications[0].counted, false);
|
|
41
|
+
assert.equal(result.applications[0].failed, true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('notification failure cannot invalidate independent browser confirmation', () => {
|
|
45
|
+
const result = deliveryProjection([application({ source: 'ashby' })], [delivery()]);
|
|
46
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
47
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('temporary delivery delays and ambiguous failures do not silently change totals', () => {
|
|
51
|
+
for (const evidenceType of ['delivery-delay', 'ambiguous']) {
|
|
52
|
+
const result = deliveryProjection([application()], [delivery({ evidenceType })]);
|
|
53
|
+
assert.equal(result.effectiveSubmissionCount, 1, evidenceType);
|
|
54
|
+
assert.equal(result.failedDeliveryCount, 0, evidenceType);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('unrelated bounce and hiring rejection do not invalidate the submitted application', () => {
|
|
59
|
+
const result = deliveryProjection([application({ outcome: 'rejected' })], [delivery({ applicationId: 'another-application', attemptId: 'initial:another-application' })]);
|
|
60
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
61
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('receipt confirmation resolves unknown email receipt', () => {
|
|
65
|
+
const result = deliveryProjection([application()], [delivery({ id: 'receipt-1', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' })]);
|
|
66
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
67
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('contradictory receipt and failure remain counted and flag conflict in either arrival order', () => {
|
|
71
|
+
const failure = delivery();
|
|
72
|
+
const receipt = delivery({ id: 'receipt-1', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
73
|
+
for (const events of [[failure, receipt], [receipt, failure]]) {
|
|
74
|
+
const result = deliveryProjection([application()], events);
|
|
75
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
76
|
+
assert.equal(result.applications[0].conflict, true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('explicit correction supersedes a failure without depending on arrival order', () => {
|
|
81
|
+
const failure = delivery();
|
|
82
|
+
const correction = delivery({ id: 'correction-1', type: 'correction', supersedes: failure.id, status: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
83
|
+
for (const events of [[failure, correction], [correction, failure]]) {
|
|
84
|
+
const result = deliveryProjection([application()], events);
|
|
85
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
86
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
87
|
+
assert.equal(result.applications[0].conflict, false);
|
|
88
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('correcting a mistaken failure to unknown restores the original email send', () => {
|
|
93
|
+
const result = deliveryProjection([application()], [delivery(), delivery({ id: 'correction-1', type: 'correction', supersedes: 'bounce-1', status: 'unknown', evidenceType: 'ambiguous' })]);
|
|
94
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
95
|
+
assert.equal(result.receiptUnknownEmailCount, 1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a failed email followed by confirmed ATS recovery remains one application with two attempts', () => {
|
|
99
|
+
const retry = delivery({ id: 'retry-1', attemptId: 'replacement-1', type: 'retry-confirmed', channel: 'browser', url: 'https://jobs.ashbyhq.com/example/123', evidenceType: 'browser-confirmation', occurredAt: '2026-09-13T11:00:00Z' });
|
|
100
|
+
const result = deliveryProjection([application()], [delivery(), retry, retry]);
|
|
101
|
+
assert.equal(result.applications.length, 1);
|
|
102
|
+
assert.equal(result.applications[0].attempts.length, 2);
|
|
103
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
104
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
105
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('replayed delivery event IDs cannot duplicate failures', () => {
|
|
109
|
+
const failure = delivery();
|
|
110
|
+
const result = deliveryProjection([application()], [failure, { ...failure }, failure]);
|
|
111
|
+
assert.equal(result.failedDeliveryCount, 1);
|
|
112
|
+
assert.equal(result.applications[0].attempts.length, 1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('legacy unfamiliar discovery rows remain outside the new projection', () => {
|
|
116
|
+
const result = discoveryProjection([{ id: 'legacy', reviewedCount: 19, qualifiedCount: 2 }, { type: 'unknown-future-record', roundId: 'round-1' }], { roundId: 'round-1' });
|
|
117
|
+
assert.equal(result.reviewedCount, 0);
|
|
118
|
+
assert.equal(result.qualifiedCount, 0);
|
|
119
|
+
assert.equal(result.leads.length, 0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('replayed lead event IDs and other rounds do not inflate reviewed counts', () => {
|
|
123
|
+
const first = lead();
|
|
124
|
+
const result = discoveryProjection([first, { ...first }, lead({ id: 'other-round', roundId: 'round-2' })], { roundId: 'round-1' });
|
|
125
|
+
assert.equal(result.reviewedCount, 1);
|
|
126
|
+
assert.equal(result.qualifiedCount, 1);
|
|
127
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('explicit assessment revision replaces its prior disposition without inflating totals', () => {
|
|
131
|
+
const original = lead();
|
|
132
|
+
const revision = lead({ id: 'lead-event-2', supersedes: original.id, disposition: 'closed-stale', observedAt: '2026-09-14T10:00:00Z' });
|
|
133
|
+
for (const events of [[original, revision], [revision, original]]) {
|
|
134
|
+
const result = discoveryProjection(events, { roundId: 'round-1' });
|
|
135
|
+
assert.equal(result.reviewedCount, 1);
|
|
136
|
+
assert.equal(result.qualifiedCount, 0);
|
|
137
|
+
assert.equal(result.conflicts.length, 0);
|
|
138
|
+
assert.equal(result.leads[0].disposition, 'closed-stale');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('competing assessment revisions surface a conflict regardless of arrival order', () => {
|
|
143
|
+
const first = lead();
|
|
144
|
+
const conflicting = lead({ id: 'lead-event-2', disposition: 'blocked' });
|
|
145
|
+
for (const events of [[first, conflicting], [conflicting, first]]) {
|
|
146
|
+
const result = discoveryProjection(events, { roundId: 'round-1' });
|
|
147
|
+
assert.equal(result.reviewedCount, 1);
|
|
148
|
+
assert.equal(result.conflicts.length, 1);
|
|
149
|
+
assert.equal(result.qualifiedCount, 0);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('cross-source sightings preserve source reviews while deduplicating the requisition', () => {
|
|
154
|
+
const result = discoveryProjection([lead({ employerJobId: 'example:123' }), lead({ id: 'linkedin-sighting', sourceId: 'linkedin', url: 'https://linkedin.com/jobs/view/987', employerJobId: 'example:123' })], { roundId: 'round-1' });
|
|
155
|
+
assert.equal(result.reviewedCount, 2);
|
|
156
|
+
assert.equal(result.qualifiedCount, 2);
|
|
157
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
158
|
+
assert.equal(result.leads.length, 2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('distinct employer requisitions remain separate even for the same company and role', () => {
|
|
162
|
+
const result = discoveryProjection([lead({ employerJobId: 'example:123' }), lead({ id: 'second-role', url: 'https://example.com/jobs/456', employerJobId: 'example:456' })], { roundId: 'round-1' });
|
|
163
|
+
assert.equal(result.reviewedCount, 2);
|
|
164
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('competing corrections cannot silently choose failure over an unknown disposition', () => {
|
|
168
|
+
const original = delivery();
|
|
169
|
+
const unknown = delivery({id:'clear-bounce',type:'correction',supersedes:original.id,status:'unknown'});
|
|
170
|
+
const failed = delivery({id:'keep-bounce',type:'correction',supersedes:original.id,status:'delivery-failed'});
|
|
171
|
+
const result = deliveryProjection([application()], [original, unknown, failed]);
|
|
172
|
+
assert.equal(result.applications[0].conflict, true);
|
|
173
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('verified requisition aliases with the same assessment count as one lead without conflict', () => {
|
|
177
|
+
const result = discoveryProjection([lead({employerJobId:'REQ-1'}),lead({id:'alias',url:'https://ats.example.com/REQ-1',employerJobId:'REQ-1'})], {roundId:'round-1'});
|
|
178
|
+
assert.equal(result.reviewedCount,1);
|
|
179
|
+
assert.equal(result.qualifiedCount,1);
|
|
180
|
+
assert.equal(result.conflicts.length,0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('a later verified requisition ID enriches a URL-only lead without inflating counts', () => {
|
|
184
|
+
const first = lead();
|
|
185
|
+
const enriched = lead({ id:'enriched',employerJobId:'REQ-1',supersedes:first.id });
|
|
186
|
+
const result = discoveryProjection([first,enriched],{roundId:'round-1'});
|
|
187
|
+
assert.equal(result.reviewedCount,1);
|
|
188
|
+
assert.equal(result.uniqueLeadCount,1);
|
|
189
|
+
assert.equal(result.leads[0].employerJobId,'REQ-1');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('conflicting retry copies project identically in either arrival order', () => {
|
|
193
|
+
const email = delivery({ id: 'retry-email', type: 'retry-confirmed', attemptId: 'replacement', channel: 'email', evidenceType: 'sent-email' });
|
|
194
|
+
const browser = { ...email, id: 'retry-browser', channel: 'browser', evidenceType: 'browser-confirmation' };
|
|
195
|
+
const forward = deliveryProjection([application()], [delivery(), email, browser]);
|
|
196
|
+
assert.deepEqual(forward, deliveryProjection([application()], [browser, email, delivery()]));
|
|
197
|
+
assert.equal(forward.applications[0].conflict, true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('company formatting does not split verified requisitions or create assessment conflicts', () => {
|
|
201
|
+
const original = lead({ company: 'Example, Inc.', employerJobId: 'REQ-1' });
|
|
202
|
+
const alias = lead({ id: 'alias', company: 'Example Inc', employerJobId: 'req-1', url: 'https://ats.example/jobs/1' });
|
|
203
|
+
const sameSource = discoveryProjection([original, alias]);
|
|
204
|
+
assert.equal(sameSource.reviewedCount, 1);
|
|
205
|
+
assert.equal(sameSource.qualifiedCount, 1);
|
|
206
|
+
const crossSource = discoveryProjection([original, { ...alias, sourceId: 'linkedin' }]);
|
|
207
|
+
assert.equal(crossSource.reviewedCount, 2);
|
|
208
|
+
assert.equal(crossSource.uniqueLeadCount, 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('URL-only sightings discard tracking while retaining distinct requisition parameters', () => {
|
|
212
|
+
const original = lead({ url: 'https://example.com/careers?jobId=123&trackingId=one' });
|
|
213
|
+
const repeat = lead({ id: 'repeat', url: 'https://example.com/careers?trackingId=two&jobId=123' });
|
|
214
|
+
assert.equal(discoveryProjection([original, repeat]).reviewedCount, 1);
|
|
215
|
+
assert.equal(discoveryProjection([original, { ...repeat, sourceId: 'linkedin' }]).uniqueLeadCount, 1);
|
|
216
|
+
assert.equal(discoveryProjection([original, { ...repeat, url: 'https://example.com/careers?jobId=456' }]).uniqueLeadCount, 2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('a revision can enrich but cannot remove a verified requisition identity', async () => {
|
|
220
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
221
|
+
const original = lead({ employerJobId: 'REQ-1' });
|
|
222
|
+
const weakened = lead({ id: 'weakened', supersedes: original.id });
|
|
223
|
+
assert.throws(() => validateLeadReferences(weakened, [original]), /same round, source and requisition/);
|
|
224
|
+
const urlOnly = lead();
|
|
225
|
+
const enriched = lead({ id: 'enriched', employerJobId: 'REQ-1', supersedes: urlOnly.id });
|
|
226
|
+
assert.doesNotThrow(() => validateLeadReferences(enriched, [urlOnly]));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('prepared retry confirmation preserves evidence without reauthorizing its transmission', async () => {
|
|
230
|
+
const { validateDeliveryReferences } = await import('../scripts/application-accounting.mjs');
|
|
231
|
+
const receipt = delivery({ id: 'receipt', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
232
|
+
const retry = delivery({ id: 'retry', type: 'retry-confirmed', attemptId: 'replacement', channel: 'browser', evidenceType: 'browser-confirmation' });
|
|
233
|
+
const evidence = [delivery(), receipt];
|
|
234
|
+
assert.throws(() => validateDeliveryReferences(retry, [application()], evidence), /verified failure/);
|
|
235
|
+
assert.doesNotThrow(() => validateDeliveryReferences(retry, [application()], evidence, { preparedRetry: true }));
|
|
236
|
+
assert.throws(() => validateDeliveryReferences({ ...retry, id: 'another' }, [application()], [...evidence, retry], { preparedRetry: true }), /attemptId already exists/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
for (const parameter of ['id', 'career_job_req_id']) {
|
|
240
|
+
test(`URL-only discovery preserves distinct ${parameter} requisition values`, () => {
|
|
241
|
+
const first = lead({ url: `https://example.com/careers?${parameter}=123&trackingId=one` });
|
|
242
|
+
const repeat = lead({ id: 'repeat', url: `https://example.com/careers?trackingId=two&${parameter}=123` });
|
|
243
|
+
const different = lead({ id: 'different', url: `https://example.com/careers?${parameter}=456&trackingId=three` });
|
|
244
|
+
const result = discoveryProjection([first, repeat, different]);
|
|
245
|
+
assert.equal(result.reviewedCount, 2);
|
|
246
|
+
assert.equal(result.qualifiedCount, 2);
|
|
247
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
248
|
+
assert.equal(result.conflicts.length, 0);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
test('a distinct requisition at a shared URL cannot undo explicit identity enrichment', async () => {
|
|
253
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
254
|
+
const original = lead({ id: 'url-observation', url: 'https://example.com/careers' });
|
|
255
|
+
const enriched = lead({ ...original, id: 'verified-requisition', employerJobId: 'REQ-1', supersedes: original.id });
|
|
256
|
+
const another = lead({ ...original, id: 'other-requisition', employerJobId: 'REQ-2' });
|
|
257
|
+
assert.doesNotThrow(() => validateLeadReferences(enriched, [original]));
|
|
258
|
+
assert.doesNotThrow(() => validateLeadReferences(another, [original, enriched]));
|
|
259
|
+
for (const events of [[original, enriched, another], [another, enriched, original]]) {
|
|
260
|
+
const result = discoveryProjection(events);
|
|
261
|
+
assert.equal(result.reviewedCount, 2);
|
|
262
|
+
assert.equal(result.qualifiedCount, 2);
|
|
263
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
264
|
+
assert.equal(result.conflicts.length, 0);
|
|
265
|
+
assert.deepEqual(result.leads.map(item => item.employerJobId).sort(), ['REQ-1', 'REQ-2']);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('an explicit correction resolves forked identity enrichment by retaining one verified branch ID', async () => {
|
|
270
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
271
|
+
const original = lead({ id: 'original', url: 'https://example.com/careers' });
|
|
272
|
+
const first = lead({ ...original, id: 'first-branch', employerJobId: 'REQ-1', supersedes: original.id });
|
|
273
|
+
const second = lead({ ...original, id: 'second-branch', employerJobId: 'REQ-2', supersedes: original.id });
|
|
274
|
+
assert.doesNotThrow(() => validateLeadReferences(first, [original]));
|
|
275
|
+
assert.doesNotThrow(() => validateLeadReferences(second, [original, first]));
|
|
276
|
+
const prior = [original, first, second];
|
|
277
|
+
assert.equal(discoveryProjection(prior).conflicts.length, 1);
|
|
278
|
+
const correction = lead({
|
|
279
|
+
...first, id: 'resolved-identity', supersedes: [first.id, second.id],
|
|
280
|
+
evidence: 'Employer posting confirms REQ-1; the second assessment used an incorrect requisition ID.',
|
|
281
|
+
});
|
|
282
|
+
assert.doesNotThrow(() => validateLeadReferences(correction, prior));
|
|
283
|
+
for (const events of [[...prior, correction], [correction, second, first, original]]) {
|
|
284
|
+
const result = discoveryProjection(events);
|
|
285
|
+
assert.equal(result.reviewedCount, 1);
|
|
286
|
+
assert.equal(result.qualifiedCount, 1);
|
|
287
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
288
|
+
assert.equal(result.conflicts.length, 0);
|
|
289
|
+
assert.equal(result.leads[0].employerJobId, 'REQ-1');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('an identity correction cannot merge unrelated requisitions at the same company and URL', async () => {
|
|
294
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
295
|
+
const first = lead({ id: 'first-requisition', employerJobId: 'REQ-1', url: 'https://example.com/careers' });
|
|
296
|
+
const second = lead({ ...first, id: 'second-requisition', employerJobId: 'REQ-2' });
|
|
297
|
+
const correction = lead({ ...first, id: 'invalid-merge', supersedes: [first.id, second.id] });
|
|
298
|
+
assert.throws(() => validateLeadReferences(correction, [first, second]), /same round, source and requisition/);
|
|
299
|
+
const result = discoveryProjection([first, second]);
|
|
300
|
+
assert.equal(result.reviewedCount, 2);
|
|
301
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
302
|
+
assert.equal(result.conflicts.length, 0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('forked identity enrichments remain one conflicted assessment in either order', () => {
|
|
306
|
+
const original = lead();
|
|
307
|
+
const left = lead({ id: 'left-enrichment', employerJobId: 'REQ-1', supersedes: original.id });
|
|
308
|
+
const right = lead({ id: 'right-enrichment', employerJobId: 'REQ-2', supersedes: original.id });
|
|
309
|
+
for (const events of [[original, left, right], [right, left, original]]) {
|
|
310
|
+
const result = discoveryProjection(events);
|
|
311
|
+
assert.equal(result.reviewedCount, 1);
|
|
312
|
+
assert.equal(result.qualifiedCount, 0);
|
|
313
|
+
assert.equal(result.conflicts.length, 1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ASHBY_ADAPTER,
|
|
6
|
+
buildSubmitProbePlan,
|
|
7
|
+
detectAbsoluteBlockers,
|
|
8
|
+
detectConfirmation,
|
|
9
|
+
resolveAtsAdapter,
|
|
10
|
+
} from "../scripts/ats/submit-adapters.mjs";
|
|
11
|
+
import {
|
|
12
|
+
RESUME_EXIT,
|
|
13
|
+
decideResumeSubmit,
|
|
14
|
+
resumeSubmitChecklist,
|
|
15
|
+
} from "../scripts/attention-resume-submit.mjs";
|
|
16
|
+
|
|
17
|
+
const binding = {
|
|
18
|
+
version: 1,
|
|
19
|
+
attentionId: "attention-1",
|
|
20
|
+
jobUrl: "https://jobs.ashbyhq.com/livekit/application",
|
|
21
|
+
browserProfilePath: "/tmp/jaa-chrome",
|
|
22
|
+
display: ":99",
|
|
23
|
+
vncPort: 5900,
|
|
24
|
+
createdAt: "2026-09-16T00:00:00.000Z",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("resolveAtsAdapter picks Ashby for ashbyhq hosts", () => {
|
|
28
|
+
assert.equal(resolveAtsAdapter("https://jobs.ashbyhq.com/x/application").id, "ashby");
|
|
29
|
+
assert.equal(resolveAtsAdapter("https://boards.greenhouse.io/x").id, "generic");
|
|
30
|
+
assert.ok(ASHBY_ADAPTER.submitSelectors.length >= 2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("detectAbsoluteBlockers and confirmation helpers", () => {
|
|
34
|
+
assert.equal(detectAbsoluteBlockers({ pageText: "Please complete the reCAPTCHA" }).blocked, true);
|
|
35
|
+
assert.equal(detectConfirmation({
|
|
36
|
+
pageUrl: "https://jobs.ashbyhq.com/x/application-submitted",
|
|
37
|
+
}).confirmed, true);
|
|
38
|
+
assert.equal(detectConfirmation({
|
|
39
|
+
pageUrl: "https://jobs.ashbyhq.com/x/application",
|
|
40
|
+
pageText: "Thank you for applying",
|
|
41
|
+
}).confirmed, true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("decideResumeSubmit: ready to submit when clear", () => {
|
|
45
|
+
const decision = decideResumeSubmit({
|
|
46
|
+
binding,
|
|
47
|
+
snapshot: {
|
|
48
|
+
pageUrl: "https://jobs.ashbyhq.com/livekit/application",
|
|
49
|
+
submitEnabled: true,
|
|
50
|
+
leaseHeld: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
assert.equal(decision.action, "ready_to_submit");
|
|
54
|
+
assert.equal(decision.exitCode, RESUME_EXIT.READY_TO_SUBMIT);
|
|
55
|
+
assert.match(decision.message, /submit if possible/i);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("decideResumeSubmit: still blocked on captcha", () => {
|
|
59
|
+
const decision = decideResumeSubmit({
|
|
60
|
+
binding,
|
|
61
|
+
snapshot: {
|
|
62
|
+
pageUrl: "https://jobs.ashbyhq.com/livekit/application",
|
|
63
|
+
pageText: "hCaptcha challenge",
|
|
64
|
+
leaseHeld: true,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
assert.equal(decision.action, "still_blocked");
|
|
68
|
+
assert.equal(decision.exitCode, RESUME_EXIT.STILL_BLOCKED);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("decideResumeSubmit: tab drift and confirmation", () => {
|
|
72
|
+
const drift = decideResumeSubmit({
|
|
73
|
+
binding,
|
|
74
|
+
snapshot: {
|
|
75
|
+
pageUrl: "https://jobs.ashbyhq.com/livekit",
|
|
76
|
+
leaseHeld: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
assert.equal(drift.action, "tab_drift");
|
|
80
|
+
assert.equal(drift.exitCode, RESUME_EXIT.TAB_OR_BINDING);
|
|
81
|
+
|
|
82
|
+
const confirmed = decideResumeSubmit({
|
|
83
|
+
binding,
|
|
84
|
+
snapshot: {
|
|
85
|
+
pageUrl: "https://jobs.ashbyhq.com/livekit/application",
|
|
86
|
+
matchedConfirmationSelectors: ["text=/thank you/i"],
|
|
87
|
+
leaseHeld: true,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
assert.equal(confirmed.action, "submitted_confirmed");
|
|
91
|
+
assert.equal(confirmed.exitCode, RESUME_EXIT.SUBMITTED);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("decideResumeSubmit: missing binding and ambiguous submit", () => {
|
|
95
|
+
assert.equal(decideResumeSubmit({
|
|
96
|
+
binding: null,
|
|
97
|
+
snapshot: { pageUrl: "https://jobs.ashbyhq.com/livekit/application" },
|
|
98
|
+
}).exitCode, RESUME_EXIT.TAB_OR_BINDING);
|
|
99
|
+
|
|
100
|
+
const ambiguous = decideResumeSubmit({
|
|
101
|
+
binding,
|
|
102
|
+
snapshot: {
|
|
103
|
+
pageUrl: "https://jobs.ashbyhq.com/livekit/application",
|
|
104
|
+
submitAlreadyClicked: true,
|
|
105
|
+
leaseHeld: true,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
assert.equal(ambiguous.action, "submit_ambiguous");
|
|
109
|
+
assert.equal(ambiguous.exitCode, RESUME_EXIT.AWAITING_CONFIRM);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("buildSubmitProbePlan and checklist are actionable", () => {
|
|
113
|
+
const plan = buildSubmitProbePlan("https://jobs.ashbyhq.com/x/application");
|
|
114
|
+
assert.equal(plan.adapterId, "ashby");
|
|
115
|
+
assert.ok(plan.submitSelectors.includes('button[type="submit"]'));
|
|
116
|
+
const lines = resumeSubmitChecklist();
|
|
117
|
+
assert.ok(lines.some((l) => /DISPLAY=:99/i.test(l)));
|
|
118
|
+
assert.ok(lines.some((l) => /intent-confirm|ledger add/i.test(l)));
|
|
119
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
EXIT,
|
|
6
|
+
buildSignalPollUrl,
|
|
7
|
+
interpretPoll,
|
|
8
|
+
parseArgs,
|
|
9
|
+
pollAttentionSignal,
|
|
10
|
+
resolvePollConfig,
|
|
11
|
+
} from "../scripts/attention-runner-poll.mjs";
|
|
12
|
+
|
|
13
|
+
test("runner poll interprets resume/skip/abort with stable exit codes", () => {
|
|
14
|
+
assert.equal(interpretPoll({ resumeRequested: true, signal: "resume_requested" }).exitCode, EXIT.RESUME);
|
|
15
|
+
assert.equal(interpretPoll({ skipped: true, signal: "skipped" }).exitCode, EXIT.SKIPPED);
|
|
16
|
+
assert.equal(interpretPoll({ aborted: true, signal: "aborted" }).exitCode, EXIT.ABORTED);
|
|
17
|
+
assert.equal(interpretPoll({ signal: null, pending: false }).done, false);
|
|
18
|
+
assert.match(interpretPoll({ resumeRequested: true }).message, /visible confirmation/i);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("resolvePollConfig derives site origin from notify URL", () => {
|
|
22
|
+
const cfg = resolvePollConfig({
|
|
23
|
+
ATTENTION_NOTIFY_SECRET: "secret",
|
|
24
|
+
ATTENTION_NOTIFY_URL: "https://jobappagent.com/api/internal/attention-notify",
|
|
25
|
+
});
|
|
26
|
+
assert.equal(cfg.siteUrl, "https://jobappagent.com");
|
|
27
|
+
assert.equal(
|
|
28
|
+
buildSignalPollUrl(cfg.siteUrl, "attention-1"),
|
|
29
|
+
"https://jobappagent.com/api/internal/attention-signals/attention-1",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("parseArgs reads attention id, interval, timeout, once", () => {
|
|
34
|
+
const args = parseArgs(["--attention-id", "attention-x", "--interval", "3", "--timeout", "30", "--once"]);
|
|
35
|
+
assert.equal(args.attentionId, "attention-x");
|
|
36
|
+
assert.equal(args.intervalSec, 3);
|
|
37
|
+
assert.equal(args.timeoutSec, 30);
|
|
38
|
+
assert.equal(args.once, true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("pollAttentionSignal returns resume exit code and next-action message", async () => {
|
|
42
|
+
let calls = 0;
|
|
43
|
+
const result = await pollAttentionSignal({
|
|
44
|
+
attentionId: "attention-1",
|
|
45
|
+
siteUrl: "https://jobappagent.com",
|
|
46
|
+
secret: "test-secret",
|
|
47
|
+
intervalSec: 1,
|
|
48
|
+
timeoutSec: 10,
|
|
49
|
+
log: () => {},
|
|
50
|
+
sleep: async () => {},
|
|
51
|
+
fetchImpl: async () => {
|
|
52
|
+
calls += 1;
|
|
53
|
+
if (calls === 1) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
attentionId: "attention-1",
|
|
56
|
+
signal: null,
|
|
57
|
+
pending: false,
|
|
58
|
+
resumeRequested: false,
|
|
59
|
+
skipped: false,
|
|
60
|
+
aborted: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return Response.json({
|
|
64
|
+
attentionId: "attention-1",
|
|
65
|
+
signal: "resume_requested",
|
|
66
|
+
pending: true,
|
|
67
|
+
resumeRequested: true,
|
|
68
|
+
skipped: false,
|
|
69
|
+
aborted: false,
|
|
70
|
+
updatedAt: "2026-09-16T00:00:00.000Z",
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result.ok, true);
|
|
75
|
+
assert.equal(result.exitCode, EXIT.RESUME);
|
|
76
|
+
assert.equal(result.action, "resume");
|
|
77
|
+
assert.match(result.message, /renew cloud lease/i);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("pollAttentionSignal fails closed without secret or on 401", async () => {
|
|
81
|
+
const missing = await pollAttentionSignal({
|
|
82
|
+
attentionId: "attention-1",
|
|
83
|
+
siteUrl: "https://jobappagent.com",
|
|
84
|
+
secret: "",
|
|
85
|
+
log: () => {},
|
|
86
|
+
});
|
|
87
|
+
assert.equal(missing.exitCode, EXIT.ERROR);
|
|
88
|
+
|
|
89
|
+
const unauthorized = await pollAttentionSignal({
|
|
90
|
+
attentionId: "attention-1",
|
|
91
|
+
siteUrl: "https://jobappagent.com",
|
|
92
|
+
secret: "bad",
|
|
93
|
+
once: true,
|
|
94
|
+
log: () => {},
|
|
95
|
+
fetchImpl: async () => new Response("nope", { status: 401 }),
|
|
96
|
+
});
|
|
97
|
+
assert.equal(unauthorized.exitCode, EXIT.ERROR);
|
|
98
|
+
assert.match(unauthorized.error, /unauthorized/i);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("pollAttentionSignal exits skipped and aborted", async () => {
|
|
102
|
+
const skipped = await pollAttentionSignal({
|
|
103
|
+
attentionId: "attention-1",
|
|
104
|
+
siteUrl: "https://jobappagent.com",
|
|
105
|
+
secret: "s",
|
|
106
|
+
once: true,
|
|
107
|
+
log: () => {},
|
|
108
|
+
fetchImpl: async () => Response.json({
|
|
109
|
+
attentionId: "attention-1",
|
|
110
|
+
signal: "skipped",
|
|
111
|
+
pending: true,
|
|
112
|
+
resumeRequested: false,
|
|
113
|
+
skipped: true,
|
|
114
|
+
aborted: false,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
assert.equal(skipped.exitCode, EXIT.SKIPPED);
|
|
118
|
+
|
|
119
|
+
const aborted = await pollAttentionSignal({
|
|
120
|
+
attentionId: "attention-1",
|
|
121
|
+
siteUrl: "https://jobappagent.com",
|
|
122
|
+
secret: "s",
|
|
123
|
+
once: true,
|
|
124
|
+
log: () => {},
|
|
125
|
+
fetchImpl: async () => Response.json({
|
|
126
|
+
attentionId: "attention-1",
|
|
127
|
+
signal: "aborted",
|
|
128
|
+
pending: true,
|
|
129
|
+
resumeRequested: false,
|
|
130
|
+
skipped: false,
|
|
131
|
+
aborted: true,
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
assert.equal(aborted.exitCode, EXIT.ABORTED);
|
|
135
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { initialOutreach, mutateOutreach } from '../../scripts/outreach-domain.mjs';
|
|
2
|
+
export const now = '2026-09-16T10:00:00.000Z';
|
|
3
|
+
export function assessment(id = 'opportunity-1', extra = {}) {
|
|
4
|
+
return { operationId: `assess-${id}`, id, company: { name: 'Example', domain: 'example.org', aliases: [] },
|
|
5
|
+
role: 'Staff Product Engineer', channel: 'linkedin', recipient: { account: 'https://www.linkedin.com/in/example-recruiter', aliases: [] },
|
|
6
|
+
source: { kind: 'hiring-post', url: 'https://example.org/jobs/1' },
|
|
7
|
+
qualification: { active: true, companyVerified: true, eligible: true, fit: true, affiliation: true, hiringInvolvement: true },
|
|
8
|
+
gateEvidence: { active: ['role'], companyVerified: ['role'], eligible: ['role'], fit: ['role', 'experience'], affiliation: ['role'], hiringInvolvement: ['role'] },
|
|
9
|
+
evidence: [{ id: 'role', kind: 'role', source: 'https://example.org/jobs/1', observedAt: now, text: 'Active eligible role; recruiter is hiring for this team.' },
|
|
10
|
+
{ id: 'experience', kind: 'candidate', source: 'canonical-resume', observedAt: now, text: 'Verified product engineering experience.' }],
|
|
11
|
+
ranking: { hiringSignal: 4, responsibility: 3, fit: 3, freshness: 2, relationship: 0 }, ...extra };
|
|
12
|
+
}
|
|
13
|
+
export function fixture() {
|
|
14
|
+
let state = initialOutreach();
|
|
15
|
+
const run = (action, input) => { const result = mutateOutreach(state, action, input, { now, actor: 'test-host' }); state = result.state; return result.result; };
|
|
16
|
+
run('policy-enable', { operationId: 'enable', timezone: 'Asia/Kolkata' });
|
|
17
|
+
run('assess', assessment());
|
|
18
|
+
run('draft', { operationId: 'draft-1', id: 'opportunity-1', text: 'Your product engineering role fits my experience. Would a brief conversation be useful?', claimRefs: ['experience'], purpose: 'initial' });
|
|
19
|
+
return { run, get state() { return state; } };
|
|
20
|
+
}
|
|
21
|
+
export const handoff = (extra = {}) => ({ operationId: 'handoff-1', id: 'opportunity-1', draftRevision: 1, qualificationRevision: 1,
|
|
22
|
+
selectedByUser: true, recheckedAt: now, history: { kind: 'user-reported', noPriorPitch: true, checkedAt: now }, ...extra });
|
|
@@ -542,12 +542,17 @@ test('CLI emits bounded source coverage without private evidence or attribution'
|
|
|
542
542
|
assert.equal(started.code, 0, started.stderr);
|
|
543
543
|
const roundId = JSON.parse(started.stdout).roundId;
|
|
544
544
|
const sourceId = 'community-0123456789abcdef';
|
|
545
|
+
for(let i=0;i<8;i++) {
|
|
546
|
+
const result = await runCli(script,['round','lead','--stdin'],{roundId,sourceId,url:`https://fixture.example/jobs/${i}`,company:`Fixture ${i}`,disposition:i<2?'qualified':'closed-stale',observedAt:'2026-08-01T00:00:00Z',evidence:'Private lead assessment'},env);
|
|
547
|
+
assert.equal(result.code,0,result.stderr);
|
|
548
|
+
}
|
|
545
549
|
const report = await runCli(script, ['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', reviewedCount: 8, qualifiedCount: 2, evidence: 'Private search query and candidate context', applicationIds: [] }, env);
|
|
546
550
|
assert.equal(report.code, 0, report.stderr);
|
|
547
551
|
const event = captured.find((body) => body.event === 'source_checked');
|
|
548
552
|
assert.ok(event, JSON.stringify(captured));
|
|
549
553
|
assert.deepEqual(event.properties, { sourceId: 'community', status: 'searched', reviewedCount: 8, qualifiedCount: 2 });
|
|
550
554
|
assert.equal(JSON.stringify(captured).includes('Private search query'), false);
|
|
555
|
+
assert.equal(JSON.stringify(captured).includes('Private lead assessment'), false);
|
|
551
556
|
assert.equal(JSON.stringify(captured).includes(roundId), false);
|
|
552
557
|
assert.equal(JSON.stringify(captured).includes(sourceId), false);
|
|
553
558
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
evaluateNovncDisplayGuard,
|
|
8
|
+
inspectNovncTargetText,
|
|
9
|
+
NOVNC_GUARD_EXIT,
|
|
10
|
+
} from "../scripts/novnc-display-guard.mjs";
|
|
11
|
+
|
|
12
|
+
test("inspectNovncTargetText finds fill vs forbidden ports", () => {
|
|
13
|
+
const good = inspectNovncTargetText("ExecStart=/usr/bin/websockify --web=/usr/share/novnc 6080 localhost:5900");
|
|
14
|
+
assert.equal(good.websockifyTargets[0].port, 5900);
|
|
15
|
+
|
|
16
|
+
const bad = inspectNovncTargetText("ExecStart=websockify 6080 localhost:5901");
|
|
17
|
+
assert.equal(bad.websockifyTargets[0].port, 5901);
|
|
18
|
+
assert.ok(bad.forbiddenPortMentions.length >= 1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("evaluateNovncDisplayGuard fails closed on 5901", () => {
|
|
22
|
+
const fail = evaluateNovncDisplayGuard({
|
|
23
|
+
text: "websockify 6080 127.0.0.1:5901",
|
|
24
|
+
});
|
|
25
|
+
assert.equal(fail.ok, false);
|
|
26
|
+
assert.ok(fail.errors.some((e) => /5901|TigerVNC/i.test(e)));
|
|
27
|
+
|
|
28
|
+
const ok = evaluateNovncDisplayGuard({
|
|
29
|
+
text: "websockify 6080 localhost:5900\nEnvironment=DISPLAY=:99",
|
|
30
|
+
});
|
|
31
|
+
assert.equal(ok.ok, true);
|
|
32
|
+
assert.equal(ok.targetPort, 5900);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("evaluateNovncDisplayGuard accepts explicit target port", () => {
|
|
36
|
+
assert.equal(evaluateNovncDisplayGuard({ targetPort: 5900 }).ok, true);
|
|
37
|
+
assert.equal(evaluateNovncDisplayGuard({ targetPort: 5901 }).ok, false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("example novnc.service targets localhost:5900", async () => {
|
|
41
|
+
const path = fileURLToPath(new URL("../references/agent-box/novnc.service.example", import.meta.url));
|
|
42
|
+
const text = await readFile(path, "utf8");
|
|
43
|
+
const result = evaluateNovncDisplayGuard({ text });
|
|
44
|
+
assert.equal(result.ok, true);
|
|
45
|
+
assert.equal(result.targetPort, 5900);
|
|
46
|
+
assert.match(text, /localhost:5900/);
|
|
47
|
+
assert.match(text, /ExecStart=.*localhost:5900/);
|
|
48
|
+
assert.doesNotMatch(text.replace(/#.*$/gm, ""), /5901/);
|
|
49
|
+
assert.equal(NOVNC_GUARD_EXIT.FAIL, 1);
|
|
50
|
+
});
|