job-application-agent 3.4.1 → 3.5.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 +36 -2
- package/job-application-agent/SKILL.md +11 -2
- package/job-application-agent/capabilities.json +2 -1
- package/job-application-agent/references/ACCOUNTING.md +104 -0
- package/job-application-agent/references/CLOUD_STATE.md +2 -0
- package/job-application-agent/references/RUNS.md +6 -0
- package/job-application-agent/references/SCHEMAS.md +4 -0
- package/job-application-agent/scripts/application-accounting.mjs +246 -0
- package/job-application-agent/scripts/cloud-state-client.mjs +57 -7
- package/job-application-agent/scripts/job-application.mjs +126 -24
- 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/application-accounting.test.mjs +315 -0
- package/job-application-agent/tests/job-application.test.mjs +5 -0
- package/job-application-agent/tests/review-cadence.test.mjs +66 -0
- package/job-application-agent/tests/workflow-state.test.mjs +18 -2
- package/package.json +10 -4
|
@@ -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
|
+
});
|
|
@@ -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,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { buildReview } from '../scripts/job-application.mjs';
|
|
10
|
+
|
|
11
|
+
const now = new Date('2026-09-14T12:00:00Z');
|
|
12
|
+
const applications = (count, submittedAt = '2026-09-14T10:00:00Z') => Array.from({ length: count }, (_, index) => ({
|
|
13
|
+
id: `application-${index}`, company: `Company ${index}`, role: 'Engineer',
|
|
14
|
+
url: `https://example.test/jobs/${index}`, source: 'email', status: 'submitted', submittedAt,
|
|
15
|
+
}));
|
|
16
|
+
const failure = {
|
|
17
|
+
version: 1, id: 'late-bounce', applicationId: 'application-0', attemptId: 'initial:application-0',
|
|
18
|
+
type: 'delivery-failed', occurredAt: '2026-09-14T11:00:00Z',
|
|
19
|
+
evidenceType: 'final-delivery-failure', evidence: 'Final failure matched to the original application email.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('late failure does not delay hygiene review after ten more canonical submissions', () => {
|
|
23
|
+
const entries = applications(20);
|
|
24
|
+
const acknowledgements = [{ uniqueSubmissionCount: 10, maturedApplicationCount: 0 }];
|
|
25
|
+
const review = buildReview(entries, [], acknowledgements, now, [failure]);
|
|
26
|
+
assert.equal(review.effectiveSubmissionCount, 19);
|
|
27
|
+
assert.equal(review.submittedSinceLastReview, 10);
|
|
28
|
+
assert.equal(review.reviewDue, true);
|
|
29
|
+
assert.ok(review.reviewReasons.includes('submission-hygiene'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('late failure does not delay mature review while conversion still uses effective applications', () => {
|
|
33
|
+
const entries = applications(40, '2026-01-01T10:00:00Z');
|
|
34
|
+
const acknowledgements = [{ uniqueSubmissionCount: 40, maturedApplicationCount: 20 }];
|
|
35
|
+
const outcomes = [{ id: 'application-1', status: 'interview', occurredAt: '2026-02-01T10:00:00Z' }];
|
|
36
|
+
const review = buildReview(entries, outcomes, acknowledgements, now, [failure]);
|
|
37
|
+
assert.equal(review.maturedApplications, 39);
|
|
38
|
+
assert.equal(review.conversionRates.interview, 2.6);
|
|
39
|
+
assert.equal(review.reviewDue, true);
|
|
40
|
+
assert.deepEqual(review.reviewReasons, ['outcome-effectiveness']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('review acknowledgement checkpoints recorded canonical counts after delivery failure', async (t) => {
|
|
44
|
+
const dir = await mkdtemp(join(tmpdir(), 'review-cadence-'));
|
|
45
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
46
|
+
const entries = applications(20, '2026-01-01T10:00:00Z');
|
|
47
|
+
// A duplicate row must not inflate either checkpoint.
|
|
48
|
+
entries.push({ ...entries[1], id: 'duplicate-row' });
|
|
49
|
+
await writeFile(join(dir, 'applications.ndjson'), entries.map(JSON.stringify).join('\n') + '\n');
|
|
50
|
+
await writeFile(join(dir, 'delivery.ndjson'), JSON.stringify(failure) + '\n');
|
|
51
|
+
await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
|
|
52
|
+
const env = {
|
|
53
|
+
...process.env, JOB_APPLICATION_AGENT_STATE_DIR: dir,
|
|
54
|
+
JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json'),
|
|
55
|
+
JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
|
|
56
|
+
};
|
|
57
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
58
|
+
const ack = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review-ack', '--stdin'], {
|
|
59
|
+
env, encoding: 'utf8', input: JSON.stringify({ reviewedAt: now.toISOString() }),
|
|
60
|
+
}));
|
|
61
|
+
assert.equal(ack.uniqueSubmissionCount, 20);
|
|
62
|
+
assert.equal(ack.maturedApplicationCount, 20);
|
|
63
|
+
const stored = JSON.parse((await readFile(join(dir, 'reviews.ndjson'), 'utf8')).trim());
|
|
64
|
+
assert.equal(stored.uniqueSubmissionCount, 20);
|
|
65
|
+
assert.equal(stored.maturedApplicationCount, 20);
|
|
66
|
+
});
|
|
@@ -71,6 +71,11 @@ function submission(index, roundId, overrides = {}) {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function recordLeads(env, roundId, sourceId, entries, otherCount = 0) {
|
|
75
|
+
for (const app of entries) cli(env, ['round','lead','--stdin'], { roundId, sourceId, applicationId: app.id, url: app.url, employerJobId: app.employerJobId, company: app.company, role: app.role, disposition:'qualified', observedAt:app.submittedAt, evidence:'Synthetic role meets the target requirements.' });
|
|
76
|
+
for (let i=0;i<otherCount;i++) cli(env, ['round','lead','--stdin'], { roundId, sourceId, url:`https://other.fixture.example/${sourceId}/${i}`, company:`Other ${i}`, disposition:'no-relevant-opening', observedAt:'2026-08-01T00:00:00Z', evidence:'No target role on this careers page.' });
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
test('persists a scoped autonomy grant and revokes future routine transmissions', async (t) => {
|
|
75
80
|
const { directory, env } = await fixture(t, 'autonomy');
|
|
76
81
|
|
|
@@ -122,6 +127,7 @@ test('counts only unique confirmed ledger submissions for an explicit round', as
|
|
|
122
127
|
assert.equal(status.completed, false);
|
|
123
128
|
|
|
124
129
|
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring']) {
|
|
130
|
+
recordLeads(env, started.roundId, sourceId, sourceId === 'linkedin-jobs-feed' ? Array.from({ length:30 },(_,i)=>submission(i,started.roundId)) : [], sourceId === 'linkedin-jobs-feed' ? 0 : 30);
|
|
125
131
|
cli(env, ['round', 'source', '--stdin'], {
|
|
126
132
|
roundId: started.roundId, sourceId, status: 'searched', reviewedCount: 30, qualifiedCount: sourceId === 'linkedin-jobs-feed' ? 30 : 0,
|
|
127
133
|
evidence: 'Reviewed matching postings against the unchanged target; other leads did not qualify.',
|
|
@@ -142,6 +148,8 @@ test('round completion requires distinct source coverage and explains concentrat
|
|
|
142
148
|
const { roundId } = cli(env, ['round', 'start', '--stdin'], { requestedCount: 2 });
|
|
143
149
|
for (const i of [1, 2]) cli(env, ['ledger', 'add', '--stdin'], submission(i, roundId, { discoverySourceId: 'linkedin-jobs-feed' }));
|
|
144
150
|
assert.match(cliFailure(env, ['round', 'complete', '--stdin'], { roundId }).stderr, /3 distinct discovery sources/i);
|
|
151
|
+
recordLeads(env,roundId,'linkedin-jobs-feed',[submission(1,roundId),submission(2,roundId)],8);
|
|
152
|
+
recordLeads(env,roundId,'indeed',[],10);
|
|
145
153
|
const check = { roundId, sourceId: 'linkedin-jobs-feed', status: 'searched', reviewedCount: 10, qualifiedCount: 2, evidence: 'Reviewed ten relevant postings using the target constraints.' };
|
|
146
154
|
cli(env, ['round', 'source', '--stdin'], check);
|
|
147
155
|
cli(env, ['round', 'source', '--stdin'], check);
|
|
@@ -169,7 +177,10 @@ test('discovery coverage rejects invalid reports and requires attribution withou
|
|
|
169
177
|
for (const bad of [{ sourceId: 'imaginary-board' }, { qualifiedCount: 5 }, { status: 'blocked' }, { applicationIds: ['not-in-round'] }, { evidence: '' }, { privateProfile: 'forbidden' }]) {
|
|
170
178
|
assert.equal(cliFailure(env, ['round', 'source', '--stdin'], { ...check, ...bad }).status, 1);
|
|
171
179
|
}
|
|
172
|
-
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring'])
|
|
180
|
+
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring']) {
|
|
181
|
+
recordLeads(env,roundId,sourceId,[submission(1,roundId)],3);
|
|
182
|
+
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId });
|
|
183
|
+
}
|
|
173
184
|
assert.match(cliFailure(env, ['round', 'complete', '--stdin'], { roundId }).stderr, /attribution/i);
|
|
174
185
|
cli(env, ['round', 'source', '--stdin'], { ...check, applicationIds: ['round-role-1'] });
|
|
175
186
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.unattributedCount, 0);
|
|
@@ -180,7 +191,11 @@ test('source diversity is independent of ATS and concentration explanations are
|
|
|
180
191
|
const { env } = await fixture(t, 'balanced-discovery');
|
|
181
192
|
const { roundId } = cli(env, ['round', 'start', '--stdin'], { requestedCount: 5 });
|
|
182
193
|
const ids = ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring'];
|
|
183
|
-
for (const sourceId of ids)
|
|
194
|
+
for (const sourceId of ids) {
|
|
195
|
+
const entries=Array.from({length:5},(_,i)=>submission(i+1,roundId)).filter((_,i)=>ids[i%3]===sourceId);
|
|
196
|
+
recordLeads(env,roundId,sourceId,entries,1);
|
|
197
|
+
cli(env, ['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', evidence: 'Reviewed relevant postings and verified target fit.' });
|
|
198
|
+
}
|
|
184
199
|
for (let i = 1; i <= 5; i++) cli(env, ['ledger', 'add', '--stdin'], submission(i, roundId, { discoverySourceId: ids[(i - 1) % 3], applicationChannel: 'ashby' }));
|
|
185
200
|
const completed = cli(env, ['round', 'complete', '--stdin'], { roundId });
|
|
186
201
|
assert.equal(completed.discovery.maxSourceSharePercent, 40);
|
|
@@ -197,6 +212,7 @@ test('blocked-only attempts and two views of the same network do not satisfy dis
|
|
|
197
212
|
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId: 'indeed' });
|
|
198
213
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.coverageSatisfied, false);
|
|
199
214
|
const { blocker, ...searched } = check;
|
|
215
|
+
recordLeads(env,roundId,'indeed',[submission(1,roundId)]);
|
|
200
216
|
cli(env, ['round', 'source', '--stdin'], { ...searched, sourceId: 'indeed', status: 'searched', reviewedCount: 1, qualifiedCount: 1 });
|
|
201
217
|
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId: 'indeed' });
|
|
202
218
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.coverageSatisfied, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-application-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "A privacy-first Agent Skill and CLI for evidence-based job discovery, application completion, and outcome tracking.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,9 +19,15 @@
|
|
|
19
19
|
"skill",
|
|
20
20
|
"job-search",
|
|
21
21
|
"job-application",
|
|
22
|
-
"career"
|
|
22
|
+
"career",
|
|
23
|
+
"privacy",
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"codex",
|
|
27
|
+
"resume",
|
|
28
|
+
"browser-automation"
|
|
23
29
|
],
|
|
24
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://jobappagent.com",
|
|
25
31
|
"repository": {
|
|
26
32
|
"type": "git",
|
|
27
33
|
"url": "git+https://github.com/vaibhavarora14/job-application-agent.git"
|
|
@@ -38,7 +44,7 @@
|
|
|
38
44
|
"check:native-artifacts": "node scripts/ci/validate-native-artifacts.mjs",
|
|
39
45
|
"privacy-audit": "node --test job-application-agent/tests/privacy-audit.test.mjs",
|
|
40
46
|
"smoke:package": "node scripts/smoke-package.mjs",
|
|
41
|
-
"check": "node --check job-application-agent/scripts/job-application.mjs && node --check job-application-agent/scripts/cloud-state-client.mjs && node --check job-application-agent/scripts/secret-store.mjs && node --check job-application-agent/scripts/source-community-client.mjs && node --check job-application-agent/scripts/source-community-schema.mjs && node --check job-application-agent/scripts/telemetry-client.mjs && node --check job-application-agent/scripts/telemetry-schema.mjs && node --check job-application-agent/scripts/version.mjs && node --check telemetry-worker/src/worker.mjs && node --check telemetry-worker/src/public-stats.mjs && node --check telemetry-worker/public/dashboard.js && node --check installer/src/cli.mjs && node --check installer/src/installer.mjs && node --check installer/src/runner.mjs && node --check installer/src/scheduler.mjs && node --check scripts/ci/classify-paths.mjs && node --check scripts/ci/validate-native-artifacts.mjs && node --check bin/job-application-agent.mjs && node --check state-worker/src/worker.mjs && node --check state-worker/src/backup.mjs && node --check state-worker/src/cli.mjs && node --check state-worker/bin/sync.mjs",
|
|
47
|
+
"check": "node --check job-application-agent/scripts/job-application.mjs && node --check job-application-agent/scripts/cloud-state-client.mjs && node --check job-application-agent/scripts/secret-store.mjs && node --check job-application-agent/scripts/source-community-client.mjs && node --check job-application-agent/scripts/source-community-schema.mjs && node --check job-application-agent/scripts/telemetry-client.mjs && node --check job-application-agent/scripts/telemetry-schema.mjs && node --check job-application-agent/scripts/version.mjs && node --check telemetry-worker/src/worker.mjs && node --check telemetry-worker/src/public-stats.mjs && node --check telemetry-worker/public/dashboard.js && node --check installer/src/cli.mjs && node --check installer/src/installer.mjs && node --check installer/src/runner.mjs && node --check installer/src/scheduler.mjs && node --check scripts/ci/classify-paths.mjs && node --check scripts/ci/validate-native-artifacts.mjs && node --check bin/job-application-agent.mjs && node --check state-worker/src/worker.mjs && node --check state-worker/src/backup.mjs && node --check state-worker/src/cli.mjs && node --check state-worker/bin/sync.mjs && node --check job-application-agent/scripts/application-accounting.mjs",
|
|
42
48
|
"prepack": "npm run check && npm test && npm run privacy-audit"
|
|
43
49
|
},
|
|
44
50
|
"engines": {
|