job-application-agent 2.0.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.
@@ -0,0 +1,432 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync, spawn } from 'node:child_process';
3
+ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryJobAssessed, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
9
+
10
+ const target = {
11
+ name: 'Test Candidate',
12
+ email: 'candidate@example.com',
13
+ phone: '+1 555 0100',
14
+ location: 'Toronto, Canada',
15
+ workAuthorization: 'Canada',
16
+ roleFamilies: ['product-engineering', 'full-stack', 'ai-ml'],
17
+ seniority: ['senior', 'staff'],
18
+ skills: ['TypeScript', 'Python', 'React'],
19
+ targetLocations: ['Canada', 'Remote'],
20
+ excludedLocations: ['United States only'],
21
+ workModes: ['remote'],
22
+ industries: ['AI'],
23
+ excludedCompanies: ['Blocked Corp'],
24
+ submissionMode: 'review-each',
25
+ yearsExperience: 10,
26
+ autoSubmitMinScore: 80,
27
+ manualReviewMinScore: 70,
28
+ minMustHaveCoverage: 70,
29
+ };
30
+
31
+ const matchingJob = {
32
+ title: 'Senior Product Engineer',
33
+ company: 'Example AI',
34
+ description: 'Build AI products with TypeScript, React and Python.',
35
+ source: 'greenhouse',
36
+ eligibility: 'eligible',
37
+ postingStatus: 'active',
38
+ roleFamily: 'product-engineering',
39
+ seniority: 'senior',
40
+ workMode: 'remote',
41
+ remote: true,
42
+ locations: ['Remote', 'Canada'],
43
+ mustHaves: [
44
+ { requirement: 'TypeScript', status: 'met', evidence: 'Resume skills and shipped products' },
45
+ { requirement: 'React', status: 'met', evidence: 'Multiple production roles' },
46
+ { requirement: 'Python', status: 'partial', evidence: 'Snorkel and Juvoxa' },
47
+ ],
48
+ };
49
+
50
+ function runCli(script, args, input, env) {
51
+ return new Promise((resolve) => {
52
+ const child = spawn(process.execPath, [script, ...args], { env, stdio: ['pipe', 'pipe', 'pipe'] });
53
+ let stdout = '';
54
+ let stderr = '';
55
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
56
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
57
+ child.on('close', (code) => resolve({ code, stdout, stderr }));
58
+ child.stdin.end(JSON.stringify(input));
59
+ });
60
+ }
61
+
62
+ test('validates a candidate-defined target profile', () => {
63
+ assert.equal(validateProfile(target).name, 'Test Candidate');
64
+ assert.throws(() => validateProfile({ ...target, roleFamilies: [] }), /non-empty/);
65
+ assert.throws(() => validateProfile({ ...target, submissionMode: 'always' }), /review-each/);
66
+ });
67
+
68
+ test('migrates a legacy profile without discarding identity or salary preference', () => {
69
+ const legacy = {
70
+ name: 'Test Candidate', email: 'candidate@example.com', phone: '+1 555 0100',
71
+ location: 'Bengaluru, India', workAuthorization: 'India', linkedin: 'https://linkedin.com/in/example',
72
+ salaryPreference: 'INR 90 lakh annually',
73
+ };
74
+ const migrated = migrateProfile(legacy, {});
75
+ assert.equal(migrated.name, legacy.name);
76
+ assert.equal(migrated.targetCompensation, legacy.salaryPreference);
77
+ assert.deepEqual(migrated.roleFamilies, ['product-engineering', 'full-stack', 'ai-ml']);
78
+ assert.deepEqual(migrated.seniority, ['senior', 'staff']);
79
+ assert.equal(migrated.submissionMode, 'routine-auto');
80
+ assert.equal(migrated.autoSubmitMinScore, 80);
81
+ assert.equal(migrated.manualReviewMinScore, 70);
82
+ assert.equal(migrated.minMustHaveCoverage, 70);
83
+ assert.equal(profileStatus(legacy).configured, false);
84
+ assert.ok(profileStatus(legacy).legacyFields.includes('salaryPreference'));
85
+ assert.equal(profileStatus(migrated).configured, true);
86
+ const customized = migrateProfile({ ...migrated, autoSubmitMinScore: 85 }, {});
87
+ assert.equal(customized.autoSubmitMinScore, 85);
88
+ });
89
+
90
+ test('scores a matching role from candidate preferences', () => {
91
+ const result = scoreJob(matchingJob, target);
92
+ assert.equal(result.decision, 'review');
93
+ assert.equal(result.autoEligible, true);
94
+ assert.equal(result.mustHaveCoverage, 83);
95
+ assert.ok(result.score >= 80);
96
+ });
97
+
98
+ test('applies posting, eligibility, work-mode, seniority and evidence gates before auto-submit', () => {
99
+ assert.equal(scoreJob({ ...matchingJob, postingStatus: 'closed' }, target).decision, 'exclude');
100
+ assert.equal(scoreJob({ ...matchingJob, postingStatus: 'unclear' }, target).decision, 'ask');
101
+ assert.equal(scoreJob({ ...matchingJob, eligibility: 'ineligible' }, target).decision, 'exclude');
102
+ assert.equal(scoreJob({ ...matchingJob, eligibility: 'unclear' }, target).decision, 'ask');
103
+ assert.equal(scoreJob({ ...matchingJob, workMode: 'onsite' }, target).decision, 'exclude');
104
+ assert.equal(scoreJob({ ...matchingJob, seniority: 'principal' }, target).decision, 'skip');
105
+ assert.equal(scoreJob({ ...matchingJob, mustHaves: undefined }, target).decision, 'ask');
106
+ assert.equal(scoreJob({ ...matchingJob, mustHaves: [{ requirement: 'Rust', status: 'unclear' }] }, target).decision, 'ask');
107
+ assert.equal(scoreJob({ ...matchingJob, mustHaves: [{ requirement: 'Rust', status: 'missing' }] }, target).decision, 'skip');
108
+ });
109
+
110
+ test('keeps 70-79 scores manual and caps experience mismatches below auto-submit', () => {
111
+ const manualRequirements = Array.from({ length: 10 }, (_, index) => ({ requirement: `Requirement ${index + 1}`, status: index < 7 ? 'met' : 'missing' }));
112
+ const manual = scoreJob({ ...matchingJob, description: 'General software products', mustHaves: manualRequirements }, target);
113
+ assert.equal(manual.decision, 'review');
114
+ assert.equal(manual.autoEligible, false);
115
+ assert.ok(manual.score >= 70 && manual.score < 80);
116
+
117
+ const overlevel = scoreJob({ ...matchingJob, experienceMin: 2, experienceMax: 5 }, target);
118
+ assert.equal(overlevel.decision, 'review');
119
+ assert.equal(overlevel.autoEligible, false);
120
+ assert.ok(overlevel.score <= 79);
121
+
122
+ const customTarget = { ...target, seniority: [...target.seniority, 'lead'] };
123
+ const lead = scoreJob({ ...matchingJob, seniority: 'lead' }, customTarget);
124
+ assert.equal(lead.decision, 'review');
125
+ assert.equal(lead.autoEligible, false);
126
+ });
127
+
128
+ test('skips published compensation below a comparable configured floor', () => {
129
+ const compensatedTarget = { ...target, compensationFloor: { amount: 9000000, currency: 'INR', period: 'year' } };
130
+ assert.equal(scoreJob({ ...matchingJob, salaryMaximum: 6000000, salaryCurrency: 'INR' }, compensatedTarget).decision, 'skip');
131
+ const unknown = scoreJob({ ...matchingJob, salaryMaximum: undefined, salaryCurrency: undefined }, compensatedTarget);
132
+ assert.equal(unknown.decision, 'review');
133
+ assert.equal(unknown.autoEligible, true);
134
+ });
135
+
136
+ test('regresses the five confirmed rejection patterns', () => {
137
+ const dave = scoreJob({ ...matchingJob, company: 'Dave Evans', title: 'Founding Engineer', postingStatus: 'closed', seniority: 'founding' }, target);
138
+ assert.equal(dave.decision, 'exclude');
139
+
140
+ const launchDarkly = scoreJob({ ...matchingJob, company: 'LaunchDarkly', eligibility: 'ineligible' }, target);
141
+ assert.equal(launchDarkly.decision, 'exclude');
142
+
143
+ const playPower = scoreJob({ ...matchingJob, company: 'PlayPower Labs', experienceMin: 2, experienceMax: 5 }, target);
144
+ assert.equal(playPower.decision, 'review');
145
+ assert.equal(playPower.autoEligible, false);
146
+ assert.ok(playPower.score <= 79);
147
+
148
+ const railway = scoreJob({
149
+ ...matchingJob,
150
+ company: 'Railway',
151
+ mustHaves: [
152
+ { requirement: 'TypeScript', status: 'met' },
153
+ { requirement: 'React product architecture', status: 'met' },
154
+ { requirement: 'Complex asynchronous deployment jobs', status: 'partial' },
155
+ { requirement: 'GraphQL', status: 'missing' },
156
+ { requirement: 'Temporal or Rust', status: 'missing' },
157
+ ],
158
+ }, target);
159
+ assert.equal(railway.decision, 'skip');
160
+ assert.equal(railway.autoEligible, false);
161
+
162
+ const holepunch = scoreJob({
163
+ ...matchingJob,
164
+ company: 'Holepunch',
165
+ title: 'Senior Node.js Software Engineer',
166
+ postingStatus: 'closed',
167
+ mustHaves: [{ requirement: 'P2P networking', status: 'missing' }],
168
+ }, target);
169
+ assert.equal(holepunch.decision, 'exclude');
170
+ });
171
+
172
+ test('excludes explicit ineligibility and candidate exclusions', () => {
173
+ assert.equal(scoreJob({
174
+ title: 'Staff Backend Engineer', company: 'Example', description: 'Python', source: 'lever', eligibility: 'ineligible', remote: true, locations: ['Canada'],
175
+ }, target).decision, 'exclude');
176
+ assert.equal(scoreJob({
177
+ title: 'Staff Backend Engineer', company: 'Blocked Corp Ltd', description: 'Python', source: 'lever', eligibility: 'eligible', remote: true, locations: ['Canada'],
178
+ }, target).decision, 'exclude');
179
+ });
180
+
181
+ test('pauses on unclear eligibility', () => {
182
+ const result = scoreJob({
183
+ title: 'Senior Backend Engineer', company: 'Example', description: 'Python', source: 'company', eligibility: 'unclear', remote: true, locations: ['Remote'],
184
+ }, target);
185
+ assert.equal(result.decision, 'ask');
186
+ });
187
+
188
+ test('accepts only confirmed submission ledger shapes', () => {
189
+ const entry = {
190
+ id: 'example-senior-product-engineer-2026-01-15',
191
+ company: 'Example',
192
+ role: 'Senior Product Engineer',
193
+ url: 'https://jobs.example.com/123',
194
+ source: 'company',
195
+ score: 80,
196
+ status: 'submitted',
197
+ submittedAt: '2026-01-15T10:00:00.000Z',
198
+ approval: 'APPROVE SUBMIT',
199
+ answers: {},
200
+ };
201
+ assert.deepEqual(validateLedgerEntry(entry), entry);
202
+ assert.equal(validateLedgerEntry({ ...entry, employerJobId: 'greenhouse:123' }).employerJobId, 'greenhouse:123');
203
+ assert.equal(validateLedgerEntry({ ...entry, id: 'workday-role', source: 'workday' }).source, 'workday');
204
+ assert.throws(() => validateLedgerEntry({ ...entry, status: 'draft' }), /submitted/);
205
+ });
206
+
207
+ test('validates structured submission metrics without adding them to the ledger shape', () => {
208
+ const metrics = { durationBucket: '5-15m', fieldsFilled: 14, shortAnswerCount: 2, resumeUploaded: true };
209
+ assert.deepEqual(validateSubmissionTelemetry(metrics), metrics);
210
+ assert.throws(() => validateSubmissionTelemetry({ ...metrics, note: 'private text' }), /unknown/i);
211
+ assert.throws(() => validateSubmissionTelemetry({ ...metrics, fieldsFilled: 1.5 }), /integer/i);
212
+ });
213
+
214
+ test('requires review at each ten confirmed submissions', () => {
215
+ const entries = Array.from({ length: 10 }, (_, index) => ({ status: 'submitted', submittedAt: `2026-01-${String(index + 1).padStart(2, '0')}T10:00:00Z` }));
216
+ assert.equal(buildReview(entries).reviewDue, true);
217
+ assert.equal(buildReview(entries.slice(0, 9)).reviewDue, false);
218
+ const withOutcome = entries.map((entry, index) => index === 0 ? { ...entry, status: 'interview' } : entry);
219
+ assert.equal(buildReview(withOutcome).submittedTotal, 10);
220
+ assert.equal(buildReview(withOutcome).outcomeCounts.interview, 1);
221
+ assert.equal(buildReview(withOutcome).reviewDue, true);
222
+ });
223
+
224
+ test('deduplicates ledger entries by normalized URL', async (t) => {
225
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-'));
226
+ t.after(() => rm(directory, { recursive: true, force: true }));
227
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
228
+ const entry = {
229
+ id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123?utm_source=x', source: 'company', score: 80, status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
230
+ telemetry: { durationBucket: '5-15m', fieldsFilled: 14, shortAnswerCount: 2, resumeUploaded: true },
231
+ };
232
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
233
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
234
+ execFileSync(process.execPath, [script, 'ledger', 'add', '--stdin'], { input: JSON.stringify(entry), env, encoding: 'utf8' });
235
+ const duplicate = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], { input: JSON.stringify({ id: 'different', url: 'https://jobs.example.com/123?ref=friend' }), env, encoding: 'utf8' }));
236
+ assert.equal(duplicate.duplicate, true);
237
+ const relabelled = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
238
+ input: JSON.stringify({ id: 'different-again', company: 'Example Holdings', role: 'Staff Engineer', url: 'https://jobs.example.com/123?candidateTracking=opaque-value' }), env, encoding: 'utf8',
239
+ }));
240
+ assert.equal(relabelled.duplicate, true);
241
+ assert.equal((await readFile(join(directory, 'applications.ndjson'), 'utf8')).includes('telemetry'), false);
242
+ assert.equal((await stat(join(directory, 'applications.ndjson'))).mode & 0o777, 0o600);
243
+ });
244
+
245
+ test('warns on same-company role matches and serializes concurrent duplicate submissions', async (t) => {
246
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-dedup-'));
247
+ t.after(() => rm(directory, { recursive: true, force: true }));
248
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
249
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
250
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
251
+ const base = {
252
+ company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
253
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
254
+ };
255
+ const [first, second] = await Promise.all([
256
+ runCli(script, ['ledger', 'add', '--stdin'], { ...base, id: 'example-role-1' }, env),
257
+ runCli(script, ['ledger', 'add', '--stdin'], { ...base, id: 'example-role-2', url: 'https://jobs.example.com/123?ref=other' }, env),
258
+ ]);
259
+ assert.deepEqual([first.code, second.code].sort(), [0, 1]);
260
+ assert.equal((await readFile(join(directory, 'applications.ndjson'), 'utf8')).trim().split('\n').length, 1);
261
+
262
+ const possible = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
263
+ input: JSON.stringify({ id: 'new-requisition', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/456' }), env, encoding: 'utf8',
264
+ }));
265
+ assert.equal(possible.duplicate, false);
266
+ assert.equal(possible.possibleDuplicate, true);
267
+ });
268
+
269
+ test('records structured outcomes idempotently without duplicate rows', async (t) => {
270
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcomes-'));
271
+ t.after(() => rm(directory, { recursive: true, force: true }));
272
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
273
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
274
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
275
+ id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
276
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
277
+ })}\n`);
278
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
279
+ const outcome = { id: 'example-role-1', status: 'rejected', occurredAt: '2026-01-20T09:00:00Z', note: 'No sponsorship' };
280
+ const enriched = { ...outcome, reasons: [{ category: 'eligibility', evidence: 'explicit' }] };
281
+ const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(outcome), env, encoding: 'utf8' }));
282
+ const second = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(enriched), env, encoding: 'utf8' }));
283
+ const third = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(enriched), env, encoding: 'utf8' }));
284
+ assert.equal(first.recorded, true);
285
+ assert.equal(second.recorded, true);
286
+ assert.equal(second.enriched, true);
287
+ assert.equal(third.recorded, false);
288
+ assert.equal(third.duplicate, true);
289
+ assert.equal((await readFile(join(directory, 'outcomes.ndjson'), 'utf8')).trim().split('\n').length, 2);
290
+ });
291
+
292
+ test('records bounded interview quality and failure-point enrichment idempotently', async (t) => {
293
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-interview-quality-'));
294
+ t.after(() => rm(directory, { recursive: true, force: true }));
295
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
296
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
297
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
298
+ id: 'example-role-1', company: 'Example', role: 'Staff Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 91,
299
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
300
+ })}\n`);
301
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
302
+ const base = { id: 'example-role-1', status: 'interview', occurredAt: '2026-01-20T09:00:00Z' };
303
+ const enriched = { ...base, interviewQuality: 'weak', failurePoint: 'role-scope' };
304
+ const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(base), env, encoding: 'utf8' }));
305
+ const second = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(enriched), env, encoding: 'utf8' }));
306
+ const third = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(enriched), env, encoding: 'utf8' }));
307
+ assert.equal(first.recorded, true);
308
+ assert.equal(second.recorded, true);
309
+ assert.equal(second.enriched, true);
310
+ assert.equal(third.duplicate, true);
311
+ const rows = (await readFile(join(directory, 'outcomes.ndjson'), 'utf8')).trim().split('\n').map(JSON.parse);
312
+ assert.equal(rows.length, 2);
313
+ assert.equal(rows[1].interviewQuality, 'weak');
314
+ assert.equal(rows[1].failurePoint, 'role-scope');
315
+
316
+ const invalid = await runCli(script, ['ledger', 'outcome', '--stdin'], { ...base, occurredAt: '2026-01-21T09:00:00Z', interviewQuality: 'excellent' }, env);
317
+ assert.equal(invalid.code, 1);
318
+ assert.match(invalid.stderr, /interviewQuality/i);
319
+ });
320
+
321
+ test('builds canonical mature-cohort reviews and honors explicit acknowledgement', () => {
322
+ const old = Array.from({ length: 20 }, (_, index) => ({
323
+ id: `old-${index}`, company: `Company ${index}`, role: 'Senior Engineer', url: `https://jobs.example.com/${index}`,
324
+ status: 'submitted', submittedAt: '2025-12-01T10:00:00Z', source: 'company', score: 80, approval: 'STANDING AUTHORIZATION', answers: {},
325
+ }));
326
+ const recent = Array.from({ length: 2 }, (_, index) => ({
327
+ id: `recent-${index}`, company: `Recent ${index}`, role: 'Staff Engineer', url: `https://jobs.example.com/recent-${index}`,
328
+ status: 'submitted', submittedAt: '2026-01-19T10:00:00Z', source: 'company', score: 85, approval: 'STANDING AUTHORIZATION', answers: {},
329
+ }));
330
+ const duplicate = { ...old[0], id: 'old-0-duplicate', url: 'https://another-ats.example.com/requisition/old-0' };
331
+ const outcomes = [
332
+ { id: 'old-0', status: 'interview', occurredAt: '2025-12-10T10:00:00Z', interviewQuality: 'promising' },
333
+ { id: 'old-1', status: 'rejected', occurredAt: '2025-12-11T10:00:00Z', interviewQuality: 'dead', failurePoint: 'constraints', reasons: [{ category: 'must-have-gap', evidence: 'inferred' }] },
334
+ ];
335
+ const now = new Date('2026-01-20T12:00:00Z');
336
+ const review = buildReview([...old, ...recent, duplicate], outcomes, [], now);
337
+ assert.equal(review.rawSubmissionRows, 23);
338
+ assert.equal(review.uniqueSubmittedTotal, 22);
339
+ assert.equal(review.duplicateSubmissionRows, 1);
340
+ assert.equal(review.maturedApplications, 20);
341
+ assert.equal(review.reviewDue, true);
342
+ assert.deepEqual(review.reviewReasons.sort(), ['outcome-effectiveness', 'submission-hygiene']);
343
+ assert.equal(review.reasonCounts['must-have-gap'], 1);
344
+ assert.equal(review.interviewQualityCounts.promising, 1);
345
+ assert.equal(review.interviewQualityCounts.dead, 1);
346
+ assert.equal(review.failurePointCounts.constraints, 1);
347
+ assert.deepEqual(review.interviewLearningSegments, [
348
+ { source: 'company', fitScoreBand: '80-89', interviewQuality: 'dead', failurePoint: 'constraints', count: 1 },
349
+ { source: 'company', fitScoreBand: '80-89', interviewQuality: 'promising', failurePoint: 'unknown', count: 1 },
350
+ ]);
351
+ assert.equal(review.matureOutcomeCounts.interview, 1);
352
+ assert.equal(review.matureOutcomeCounts.rejected, 1);
353
+ assert.equal(review.conversionRates.interview, 5);
354
+ assert.equal(review.conversionRates.rejected, 5);
355
+
356
+ const acknowledged = buildReview([...old, ...recent, duplicate], outcomes, [{
357
+ reviewedAt: '2026-01-20T11:00:00Z', uniqueSubmissionCount: 22, maturedApplicationCount: 20,
358
+ }], now);
359
+ assert.equal(acknowledged.reviewDue, false);
360
+ });
361
+
362
+ test('preserves interview quality after a later final outcome', () => {
363
+ const applications = [{
364
+ id: 'role-1', company: 'Example', role: 'Staff Engineer', url: 'https://jobs.example.com/role-1',
365
+ status: 'submitted', submittedAt: '2025-12-01T10:00:00Z', source: 'company', score: 90, approval: 'STANDING AUTHORIZATION', answers: {},
366
+ }];
367
+ const outcomes = [
368
+ { id: 'role-1', status: 'interview', occurredAt: '2025-12-10T10:00:00Z', interviewQuality: 'promising' },
369
+ { id: 'role-1', status: 'rejected', occurredAt: '2025-12-15T10:00:00Z' },
370
+ ];
371
+ const review = buildReview(applications, outcomes, [], new Date('2026-01-20T12:00:00Z'));
372
+ assert.equal(review.outcomeCounts.rejected, 1);
373
+ assert.equal(review.outcomeCounts.interview, 0);
374
+ assert.equal(review.interviewQualityCounts.promising, 1);
375
+ });
376
+
377
+ test('acknowledges a generated review only through the explicit CLI command', async (t) => {
378
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-review-'));
379
+ t.after(() => rm(directory, { recursive: true, force: true }));
380
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
381
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
382
+ const entries = Array.from({ length: 10 }, (_, index) => ({
383
+ id: `role-${index}`, company: `Company ${index}`, role: 'Senior Engineer', url: `https://jobs.example.com/${index}`,
384
+ source: 'company', score: 80, status: 'submitted', submittedAt: '2026-01-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
385
+ }));
386
+ await writeFile(join(directory, 'applications.ndjson'), `${entries.map(JSON.stringify).join('\n')}\n`);
387
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
388
+ const before = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review'], { env, encoding: 'utf8' }));
389
+ assert.equal(before.reviewDue, true);
390
+ assert.equal(await readFile(join(directory, 'reviews.ndjson'), 'utf8').catch(() => ''), '');
391
+ const ack = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review-ack', '--stdin'], {
392
+ input: JSON.stringify({ reviewedAt: '2026-02-01T10:00:00Z' }), env, encoding: 'utf8',
393
+ }));
394
+ assert.equal(ack.acknowledged, true);
395
+ const after = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review'], { env, encoding: 'utf8' }));
396
+ assert.equal(after.reviewDue, false);
397
+ });
398
+
399
+ test('maps commands and durations to bounded telemetry categories', () => {
400
+ assert.equal(commandCategory(['ledger', 'add', '--stdin']), 'apply');
401
+ assert.equal(commandCategory(['ledger', 'outcome', '--stdin']), 'outcome');
402
+ assert.equal(commandCategory(['score', '--stdin']), 'assess');
403
+ assert.equal(durationBucket(700), 'under-1s');
404
+ assert.equal(durationBucket(70_000), '1-2m');
405
+ assert.equal(durationBucket(2_000_000), '15m-plus');
406
+ });
407
+
408
+ test('builds a structured assessment event without description or candidate profile data', async () => {
409
+ const job = {
410
+ title: 'Senior Product Engineer', company: 'Example AI', description: 'private long job description', source: 'greenhouse', eligibility: 'eligible',
411
+ remote: true, locations: ['Remote'], url: 'https://jobs.example.com/123?candidate=secret',
412
+ };
413
+ const result = scoreJob(job, target);
414
+ const event = await telemetryJobAssessed(job, result);
415
+ assert.equal(event.event, 'job_assessed');
416
+ assert.equal(event.properties.company, 'Example AI');
417
+ assert.equal('description' in event.properties, false);
418
+ assert.equal(JSON.stringify(event).includes('candidate=secret'), false);
419
+ assert.equal(JSON.stringify(event).includes('Test Candidate'), false);
420
+ });
421
+
422
+ test('telemetry CLI controls are private and reset removes anonymous credentials', async (t) => {
423
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-telemetry-'));
424
+ t.after(() => rm(directory, { recursive: true, force: true }));
425
+ const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
426
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory, JOB_APPLICATION_AGENT_TELEMETRY_URL: 'https://relay.invalid' };
427
+ const disabled = JSON.parse(execFileSync(process.execPath, [script, 'telemetry', 'disable'], { env, encoding: 'utf8' }));
428
+ assert.equal(disabled.enabled, false);
429
+ const reset = JSON.parse(execFileSync(process.execPath, [script, 'telemetry', 'reset'], { env, encoding: 'utf8' }));
430
+ assert.equal(reset.hasInstallationId, false);
431
+ assert.equal((await stat(join(directory, 'telemetry.json'))).mode & 0o777, 0o600);
432
+ });
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { prepareTelemetryInput } from '../scripts/telemetry-client.mjs';
5
+ import { validateEvent } from '../scripts/telemetry-schema.mjs';
6
+
7
+ const forbiddenProperties = [
8
+ 'name', 'email', 'phone', 'exactAddress', 'linkedin', 'github', 'portfolio', 'candidateLocation',
9
+ 'workAuthorization', 'personalCompensation', 'resume', 'resumeFilename', 'resumeHash', 'prompt',
10
+ 'agentResponse', 'jobDescription', 'formQuestion', 'draftedAnswer', 'note', 'password', 'mfa',
11
+ 'captcha', 'legalAnswer', 'demographicAnswer', 'browserData', 'ipAddress', 'requestHeaders',
12
+ 'userAgent', 'rawError',
13
+ ];
14
+
15
+ test('privacy audit rejects every prohibited free-form or identity property', () => {
16
+ const base = { command: 'search', result: 'success', durationBucket: 'under-1s' };
17
+ for (const property of forbiddenProperties) {
18
+ assert.throws(() => validateEvent({ event: 'command_completed', properties: { ...base, [property]: 'private-value' } }), /unknown/i, property);
19
+ }
20
+ });
21
+
22
+ test('privacy audit rejects identity-like company and title values', () => {
23
+ const base = { jobHash: 'a'.repeat(64), domain: 'jobs.example.com', ats: 'greenhouse', fitScore: 80, eligibility: 'eligible', decision: 'review', matchTags: [], gapTags: [] };
24
+ for (const company of ['candidate@example.com', '+91 98765 43210', 'https://linkedin.com/in/candidate']) {
25
+ assert.throws(() => validateEvent({ event: 'job_assessed', properties: { ...base, company, title: 'Staff Engineer' } }), /identity/i);
26
+ }
27
+ });
28
+
29
+ test('privacy audit strips the full query and fragment before job URL hashing', async () => {
30
+ const safe = await prepareTelemetryInput({
31
+ event: 'application_started',
32
+ properties: {
33
+ jobUrl: 'https://jobs.example.com/role/123?email=candidate@example.com&token=secret#private',
34
+ ats: 'ashby', approvalMode: 'routine-auto', requiredFieldCount: 12, resumeRequired: true,
35
+ coverLetterRequired: false, referralPresent: false,
36
+ },
37
+ });
38
+ const serialized = JSON.stringify(safe);
39
+ assert.equal(serialized.includes('candidate@example.com'), false);
40
+ assert.equal(serialized.includes('secret'), false);
41
+ assert.equal(serialized.includes('jobs.example.com/role/123'), false);
42
+ });
@@ -0,0 +1,132 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { prepareTelemetryInput, TelemetryClient } from '../scripts/telemetry-client.mjs';
8
+
9
+ function fakeRelay() {
10
+ const requests = [];
11
+ const fetch = async (url, options = {}) => {
12
+ requests.push({ url, options, body: options.body ? JSON.parse(options.body) : null });
13
+ if (url.endsWith('/v1/install')) return Response.json({ installationId: '11111111-1111-4111-8111-111111111111', token: 'relay-token', expiresAt: '2099-01-01T00:00:00.000Z' });
14
+ return Response.json({ accepted: true, eventId: '22222222-2222-4222-8222-222222222222' }, { status: 202 });
15
+ };
16
+ return { requests, fetch };
17
+ }
18
+
19
+ test('new installations disclose and send the first event immediately', async (t) => {
20
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-new-'));
21
+ t.after(() => rm(directory, { recursive: true, force: true }));
22
+ const relay = fakeRelay();
23
+ let notice = '';
24
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: relay.fetch, stderr: (value) => { notice += value; } });
25
+ const session = await client.beginCommand('search');
26
+ assert.equal(session.installationEventPending, true);
27
+ await client.record({ event: 'installation_started', properties: { osFamily: 'macos', nodeMajor: 24, submissionMode: 'unconfigured' } }, session);
28
+ const result = await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: '1-5s' } }, session);
29
+ assert.match(notice, /anonymous usage analytics/i);
30
+ assert.equal(result.sent, true);
31
+ assert.equal(relay.requests.length, 3);
32
+ assert.equal((await client.beginCommand('search')).installationEventPending, false);
33
+ assert.equal((await stat(join(directory, 'telemetry.json'))).mode & 0o777, 0o600);
34
+ });
35
+
36
+ test('existing installations receive a one-command grace period without backfill', async (t) => {
37
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-existing-'));
38
+ t.after(() => rm(directory, { recursive: true, force: true }));
39
+ await writeFile(join(directory, 'applications.ndjson'), '{"private":"historical"}\n');
40
+ const relay = fakeRelay();
41
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: relay.fetch, stderr: () => {} });
42
+ const firstSession = await client.beginCommand('search');
43
+ assert.equal(firstSession.installationEventPending, false);
44
+ assert.equal((await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'under-1s' } }, firstSession)).sent, false);
45
+ assert.equal(relay.requests.length, 0);
46
+ const secondSession = await client.beginCommand('search');
47
+ assert.equal((await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'under-1s' } }, secondSession)).sent, true);
48
+ assert.equal(relay.requests.length, 2);
49
+ });
50
+
51
+ test('an interrupted existing-install disclosure still preserves the grace command', async (t) => {
52
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-grace-recovery-'));
53
+ t.after(() => rm(directory, { recursive: true, force: true }));
54
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: true, disclosed: true, graceConsumed: false, installationEventPending: false }));
55
+ const relay = fakeRelay();
56
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: relay.fetch, stderr: () => {} });
57
+ const session = await client.beginCommand('search');
58
+ assert.equal(session.allowSend, false);
59
+ assert.equal((await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'under-1s' } }, session)).reason, 'grace');
60
+ assert.equal(relay.requests.length, 0);
61
+ });
62
+
63
+ test('a pre-disclosure telemetry config cannot send on its disclosure command', async (t) => {
64
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-undisclosed-'));
65
+ t.after(() => rm(directory, { recursive: true, force: true }));
66
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: true, disclosed: false, graceConsumed: false, installationEventPending: false }));
67
+ const relay = fakeRelay();
68
+ let notice = '';
69
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: relay.fetch, stderr: (value) => { notice += value; } });
70
+ const session = await client.beginCommand('search');
71
+ assert.match(notice, /anonymous usage analytics/i);
72
+ assert.equal(session.allowSend, false);
73
+ assert.equal(relay.requests.length, 0);
74
+ });
75
+
76
+ test('disable preserves identity while reset removes it and keeps telemetry disabled', async (t) => {
77
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-controls-'));
78
+ t.after(() => rm(directory, { recursive: true, force: true }));
79
+ const relay = fakeRelay();
80
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: relay.fetch, stderr: () => {} });
81
+ const session = await client.beginCommand('search');
82
+ await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'under-1s' } }, session);
83
+ const disabled = await client.configure('disable');
84
+ assert.equal(disabled.enabled, false);
85
+ assert.equal(disabled.hasInstallationId, true);
86
+ await client.configure('enable');
87
+ const resumed = await client.beginCommand('search');
88
+ await client.record({ event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'under-1s' } }, resumed);
89
+ assert.equal(relay.requests.filter((request) => request.url.endsWith('/v1/install')).length, 1);
90
+ const reset = await client.configure('reset');
91
+ assert.equal(reset.enabled, false);
92
+ assert.equal(reset.hasInstallationId, false);
93
+ assert.equal((await client.status()).enabled, false);
94
+ });
95
+
96
+ test('preview validates but never transmits and network failures never escape', async (t) => {
97
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-preview-'));
98
+ t.after(() => rm(directory, { recursive: true, force: true }));
99
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: async () => { throw new Error('offline'); }, stderr: () => {} });
100
+ const event = { event: 'application_paused', properties: { jobHash: 'a'.repeat(64), ats: 'lever', stage: 'legal', reason: 'legal' } };
101
+ assert.equal((await client.preview(event)).event, 'application_paused');
102
+ const session = await client.beginCommand('apply');
103
+ assert.deepEqual(await client.record(event, session), { sent: false, reason: 'unavailable' });
104
+ });
105
+
106
+ test('converts a transient job URL to a hash and domain before validation', async () => {
107
+ const event = await prepareTelemetryInput({
108
+ event: 'application_paused',
109
+ properties: { jobUrl: 'https://jobs.example.com/role/123?email=candidate@example.com', ats: 'ashby', stage: 'legal', reason: 'legal' },
110
+ });
111
+ assert.equal(event.properties.domain, undefined);
112
+ assert.equal(event.properties.jobHash.length, 64);
113
+ assert.equal(JSON.stringify(event).includes('candidate@example.com'), false);
114
+ });
115
+
116
+ test('record input rejects undocumented top-level properties', async () => {
117
+ await assert.rejects(() => prepareTelemetryInput({
118
+ event: 'command_completed',
119
+ properties: { command: 'search', result: 'success', durationBucket: 'under-1s' },
120
+ prompt: 'private free-form content',
121
+ }), /unknown/i);
122
+ });
123
+
124
+ test('strict record rejects invalid schema while automatic telemetry stays best effort', async (t) => {
125
+ const directory = await mkdtemp(join(tmpdir(), 'telemetry-strict-'));
126
+ t.after(() => rm(directory, { recursive: true, force: true }));
127
+ const client = new TelemetryClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: fakeRelay().fetch, stderr: () => {} });
128
+ const session = await client.beginCommand('telemetry');
129
+ const invalid = { event: 'command_completed', properties: { command: 'search', result: 'success', durationBucket: 'raw private value' } };
130
+ await assert.rejects(() => client.record(invalid, session, { strict: true }), /durationBucket/i);
131
+ assert.deepEqual(await client.record(invalid, session), { sent: false, reason: 'invalid' });
132
+ });