job-application-agent 3.7.0 → 3.7.2

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,489 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdtemp, 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 { deliveryProjection } from '../scripts/application-accounting.mjs';
10
+ import { assessment, handoff } from './fixtures/outreach.mjs';
11
+ import {
12
+ OUTREACH_ROUND_CHANNEL,
13
+ companyCountKey,
14
+ confirmOutreachTowardRound,
15
+ invalidatedOutreachIds,
16
+ loadSentVerifiedOutreach,
17
+ projectOutreachRoundCounts,
18
+ resolveOutreachRoundId,
19
+ sentVerifiedFromOutreachItems,
20
+ } from '../scripts/outreach-round.mjs';
21
+
22
+ const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
23
+
24
+ function application(roundId, company = 'Example') {
25
+ return {
26
+ id: `app-${company.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
27
+ company,
28
+ role: 'Staff Product Engineer',
29
+ url: `https://jobs.fixture.example/${company.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
30
+ employerJobId: `example:${company}`,
31
+ source: 'ashby',
32
+ discoverySource: 'linkedin',
33
+ applicationChannel: 'ashby',
34
+ roundId,
35
+ score: 86,
36
+ status: 'submitted',
37
+ submittedAt: '2026-09-14T10:00:00.000Z',
38
+ approval: 'STANDING AUTHORIZATION',
39
+ answers: {},
40
+ };
41
+ }
42
+
43
+ function verifiedItem(id, company) {
44
+ return {
45
+ id,
46
+ delivery: 'sent-verified',
47
+ content: { assessment: { company: { name: company }, role: 'Staff Product Engineer' } },
48
+ };
49
+ }
50
+
51
+ test('sent-verified outreach increments a round when no apply exists', () => {
52
+ const delivery = deliveryProjection([], []);
53
+ const counts = projectOutreachRoundCounts({
54
+ applications: [],
55
+ delivery,
56
+ roundEvents: [{ type: 'submission-confirmed', roundId: 'round-1', outreachId: 'noon', channel: OUTREACH_ROUND_CHANNEL, company: 'Noon' }],
57
+ sentVerified: sentVerifiedFromOutreachItems([verifiedItem('noon', 'Noon')]),
58
+ roundId: 'round-1',
59
+ });
60
+ assert.equal(counts.applyConfirmationCount, 0);
61
+ assert.equal(counts.outreachConfirmationCount, 1);
62
+ assert.equal(counts.confirmedCount, 1);
63
+ });
64
+
65
+ test('apply plus outreach at the same company counts once', () => {
66
+ const applications = [application('round-1', 'Tricog')];
67
+ const delivery = deliveryProjection(applications, []);
68
+ const counts = projectOutreachRoundCounts({
69
+ applications,
70
+ delivery,
71
+ roundEvents: [{ type: 'submission-confirmed', roundId: 'round-1', outreachId: 'tricog', channel: OUTREACH_ROUND_CHANNEL, company: 'Tricog' }],
72
+ sentVerified: sentVerifiedFromOutreachItems([verifiedItem('tricog', 'Tricog')]),
73
+ roundId: 'round-1',
74
+ });
75
+ assert.equal(counts.applyConfirmationCount, 1);
76
+ assert.equal(counts.outreachConfirmationCount, 0);
77
+ assert.equal(counts.confirmedCount, 1);
78
+ });
79
+
80
+ test('cleared outreach still counts from the durable confirmation company', () => {
81
+ const counts = projectOutreachRoundCounts({
82
+ applications: [],
83
+ delivery: deliveryProjection([], []),
84
+ roundEvents: [{ type: 'submission-confirmed', roundId: 'round-1', outreachId: 'noon', channel: OUTREACH_ROUND_CHANNEL, company: 'Noon' }],
85
+ sentVerified: sentVerifiedFromOutreachItems([{
86
+ id: 'noon',
87
+ delivery: 'sent-verified',
88
+ cleared: true,
89
+ content: { assessment: { company: { name: 'Noon' }, role: 'Staff Product Engineer' } },
90
+ }]),
91
+ roundId: 'round-1',
92
+ });
93
+ assert.equal(counts.applyConfirmationCount, 0);
94
+ assert.equal(counts.outreachConfirmationCount, 1);
95
+ assert.equal(counts.confirmedCount, 1);
96
+ });
97
+
98
+ test('linked company names share one count key', () => {
99
+ assert.equal(companyCountKey('Made Card'), companyCountKey('MadeCard'));
100
+ const applications = [application('round-1', 'MadeCard')];
101
+ const delivery = deliveryProjection(applications, []);
102
+ const counts = projectOutreachRoundCounts({
103
+ applications,
104
+ delivery,
105
+ roundEvents: [{ type: 'submission-confirmed', roundId: 'round-1', outreachId: 'made', channel: OUTREACH_ROUND_CHANNEL, company: 'Made Card' }],
106
+ sentVerified: sentVerifiedFromOutreachItems([verifiedItem('made', 'Made Card')]),
107
+ roundId: 'round-1',
108
+ });
109
+ assert.equal(counts.applyConfirmationCount, 1);
110
+ assert.equal(counts.outreachConfirmationCount, 0);
111
+ assert.equal(counts.confirmedCount, 1);
112
+ });
113
+
114
+ test('explicit confirm cannot reassign an attached outreach to another round', () => {
115
+ assert.throws(() => resolveOutreachRoundId([
116
+ { type: 'started', roundId: 'round-a', requestedCount: 30, occurredAt: '2026-09-14T10:00:00.000Z' },
117
+ { type: 'submission-confirmed', roundId: 'round-a', outreachId: 'noon', channel: OUTREACH_ROUND_CHANNEL, company: 'Noon' },
118
+ { type: 'started', roundId: 'round-b', requestedCount: 30, occurredAt: '2026-09-14T11:00:00.000Z' },
119
+ ], { outreachId: 'noon', roundId: 'round-b' }), /already attached to another round/);
120
+ });
121
+
122
+ test('apply-only still increments without outreach', () => {
123
+ const applications = [application('round-1', 'MadeCard')];
124
+ const delivery = deliveryProjection(applications, []);
125
+ const counts = projectOutreachRoundCounts({
126
+ applications,
127
+ delivery,
128
+ roundEvents: [],
129
+ sentVerified: [],
130
+ roundId: 'round-1',
131
+ });
132
+ assert.equal(counts.applyConfirmationCount, 1);
133
+ assert.equal(counts.outreachConfirmationCount, 0);
134
+ assert.equal(counts.confirmedCount, 1);
135
+ });
136
+
137
+ async function fixture(t, label) {
138
+ const directory = await mkdtemp(join(tmpdir(), `outreach-round-${label}-`));
139
+ t.after(() => rm(directory, { recursive: true, force: true }));
140
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({
141
+ version: 1,
142
+ enabled: false,
143
+ disclosed: true,
144
+ graceConsumed: true,
145
+ installationEventPending: false,
146
+ }));
147
+ const env = {
148
+ ...process.env,
149
+ JOB_APPLICATION_AGENT_STATE_DIR: directory,
150
+ JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(directory, 'cloud-config.json'),
151
+ JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
152
+ };
153
+ const cli = (args, input) => JSON.parse(execFileSync(process.execPath, [script, ...args], {
154
+ env,
155
+ encoding: 'utf8',
156
+ ...(input === undefined ? {} : { input: JSON.stringify(input) }),
157
+ }));
158
+ return { directory, env, cli };
159
+ }
160
+
161
+ function recordVerifiedOutreach(cli, { id, company, domain }) {
162
+ cli(['outreach', 'policy', 'enable', '--stdin'], { operationId: `enable-${id}`, timezone: 'Asia/Kolkata' });
163
+ cli(['outreach', 'assess', '--stdin'], assessment(id, {
164
+ operationId: `assess-${id}`,
165
+ company: { name: company, domain, aliases: [] },
166
+ recipient: { account: `https://www.linkedin.com/in/${id}-recruiter`, aliases: [] },
167
+ }));
168
+ cli(['outreach', 'draft', '--stdin'], {
169
+ operationId: `draft-${id}`,
170
+ id,
171
+ text: 'Your product engineering role fits my experience. Would a brief conversation be useful?',
172
+ claimRefs: ['experience'],
173
+ purpose: 'initial',
174
+ });
175
+ const recheckedAt = new Date().toISOString();
176
+ cli(['outreach', 'handoff', '--stdin'], handoff({
177
+ operationId: `handoff-${id}`,
178
+ id,
179
+ recheckedAt,
180
+ history: { kind: 'user-reported', noPriorPitch: true, checkedAt: recheckedAt },
181
+ }));
182
+ const occurredAt = new Date().toISOString();
183
+ return cli(['outreach', 'record', '--stdin'], {
184
+ operationId: `sent-${id}`,
185
+ id,
186
+ attemptId: `handoff-${id}`,
187
+ type: 'sent-verified',
188
+ occurredAt,
189
+ evidence: 'Visible sent message in the conversation.',
190
+ messageRef: `visible-message-${id}`,
191
+ });
192
+ }
193
+
194
+ test('CLI sent-verified outreach increments the active round', async (t) => {
195
+ const { cli } = await fixture(t, 'outreach-only');
196
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
197
+ const recorded = recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
198
+ assert.equal(recorded.delivery, 'sent-verified');
199
+ assert.equal(recorded.roundConfirmation.counted, true);
200
+ assert.equal(recorded.roundConfirmation.roundId, roundId);
201
+ const status = cli(['round', 'status', roundId]);
202
+ assert.equal(status.applyConfirmationCount, 0);
203
+ assert.equal(status.outreachConfirmationCount, 1);
204
+ assert.equal(status.confirmedCount, 1);
205
+ assert.equal(status.effectiveSubmissionCount, 0);
206
+ const again = cli(['round', 'confirm', '--stdin'], { roundId, outreachId: 'noon' });
207
+ assert.equal(again.confirmations[0].reason, 'already-recorded');
208
+ assert.equal(again.confirmedCount, 1);
209
+ });
210
+
211
+ test('CLI apply plus outreach for the same company counts once', async (t) => {
212
+ const { cli } = await fixture(t, 'apply-and-outreach');
213
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
214
+ cli(['ledger', 'add', '--stdin'], application(roundId, 'Tricog'));
215
+ const recorded = recordVerifiedOutreach(cli, { id: 'tricog', company: 'Tricog', domain: 'tricog.com' });
216
+ assert.equal(recorded.roundConfirmation.counted, false);
217
+ assert.equal(recorded.roundConfirmation.reason, 'already-applied');
218
+ const status = cli(['round', 'status', roundId]);
219
+ assert.equal(status.applyConfirmationCount, 1);
220
+ assert.equal(status.outreachConfirmationCount, 0);
221
+ assert.equal(status.confirmedCount, 1);
222
+ assert.equal(status.effectiveSubmissionCount, 1);
223
+ });
224
+
225
+ test('CLI apply-only still increments the round', async (t) => {
226
+ const { cli } = await fixture(t, 'apply-only');
227
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
228
+ cli(['ledger', 'add', '--stdin'], application(roundId, 'MadeCard'));
229
+ const status = cli(['round', 'status', roundId]);
230
+ assert.equal(status.applyConfirmationCount, 1);
231
+ assert.equal(status.outreachConfirmationCount, 0);
232
+ assert.equal(status.confirmedCount, 1);
233
+ assert.equal(status.effectiveSubmissionCount, 1);
234
+ });
235
+
236
+ test('later progression records do not auto-attach a historical send', async (t) => {
237
+ const { cli } = await fixture(t, 'reply-does-not-count');
238
+ const recorded = recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
239
+ assert.equal(recorded.roundConfirmation.counted, false);
240
+ assert.equal(recorded.roundConfirmation.reason, 'no-active-round');
241
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
242
+ const reply = cli(['outreach', 'record', '--stdin'], {
243
+ operationId: 'reply-noon',
244
+ id: 'noon',
245
+ type: 'replied',
246
+ occurredAt: new Date().toISOString(),
247
+ evidence: 'Recruiter replied in the conversation.',
248
+ replyTone: 'neutral',
249
+ });
250
+ assert.equal(reply.delivery, 'sent-verified');
251
+ assert.equal(reply.roundConfirmation, undefined);
252
+ const status = cli(['round', 'status', roundId]);
253
+ assert.equal(status.outreachConfirmationCount, 0);
254
+ assert.equal(status.confirmedCount, 0);
255
+ });
256
+
257
+ test('batch confirm validates every ID before committing any', async (t) => {
258
+ const { cli } = await fixture(t, 'batch-validate');
259
+ recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
260
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
261
+ assert.throws(
262
+ () => cli(['round', 'confirm', '--stdin'], { roundId, outreachIds: ['noon', 'missing'] }),
263
+ /not sent-verified/,
264
+ );
265
+ const status = cli(['round', 'status', roundId]);
266
+ assert.equal(status.outreachConfirmationCount, 0);
267
+ assert.equal(status.confirmedCount, 0);
268
+ });
269
+
270
+ test('CLI delivery correction drops confirmedCount after a counted send', async (t) => {
271
+ const { cli } = await fixture(t, 'delivery-correction');
272
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
273
+ recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
274
+ assert.equal(cli(['round', 'status', roundId]).confirmedCount, 1);
275
+ cli(['outreach', 'record', '--stdin'], {
276
+ operationId: 'correct-noon',
277
+ id: 'noon',
278
+ attemptId: 'handoff-noon',
279
+ type: 'not-sent',
280
+ occurredAt: new Date().toISOString(),
281
+ evidence: 'Visible conversation shows the message was not sent.',
282
+ supersedes: ['sent-noon'],
283
+ });
284
+ const after = cli(['round', 'status', roundId]);
285
+ assert.equal(after.confirmedCount, 0);
286
+ assert.equal(after.outreachConfirmationCount, 0);
287
+ });
288
+
289
+ test('sent-verified confirm then clear leaves confirmedCount unchanged', async (t) => {
290
+ const { cli } = await fixture(t, 'clear-keeps-count');
291
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
292
+ const recorded = recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
293
+ assert.equal(recorded.roundConfirmation.counted, true);
294
+ const before = cli(['round', 'status', roundId]);
295
+ assert.equal(before.confirmedCount, 1);
296
+ assert.equal(before.outreachConfirmationCount, 1);
297
+ cli(['outreach', 'clear', '--stdin'], { operationId: 'clear-noon', ids: ['noon'] });
298
+ const after = cli(['round', 'status', roundId]);
299
+ assert.equal(after.confirmedCount, before.confirmedCount);
300
+ assert.equal(after.outreachConfirmationCount, 1);
301
+ assert.equal(after.applyConfirmationCount, 0);
302
+ });
303
+
304
+ test('explicit confirm rejects moving an attached outreach to another round', async (t) => {
305
+ const { cli } = await fixture(t, 'no-reassign');
306
+ const first = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
307
+ recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
308
+ const second = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
309
+ assert.throws(
310
+ () => cli(['round', 'confirm', '--stdin'], { roundId: second.roundId, outreachId: 'noon' }),
311
+ /already attached to another round/,
312
+ );
313
+ assert.equal(cli(['round', 'status', first.roundId]).confirmedCount, 1);
314
+ assert.equal(cli(['round', 'status', second.roundId]).confirmedCount, 0);
315
+ });
316
+
317
+ test('auto-attach reconciles authenticated cloud rounds before choosing a destination', async (t) => {
318
+ const directory = await mkdtemp(join(tmpdir(), 'outreach-round-reconcile-'));
319
+ t.after(() => rm(directory, { recursive: true, force: true }));
320
+ const started = { type: 'started', roundId: 'cloud-round', requestedCount: 30, occurredAt: '2026-09-14T10:00:00.000Z' };
321
+ const reconcileCalls = [];
322
+ const cloudClient = {
323
+ configured: async () => true,
324
+ status: async () => ({ configured: true, capabilities: ['outreach-tracking-v1'] }),
325
+ request: async () => ({ json: async () => ({ items: [verifiedItem('noon', 'Noon')] }) }),
326
+ reconcile: async (options) => {
327
+ reconcileCalls.push(options);
328
+ await writeFile(join(directory, 'rounds.ndjson'), `${JSON.stringify(started)}\n`);
329
+ },
330
+ appendRecord: async () => {},
331
+ };
332
+ const result = await confirmOutreachTowardRound({
333
+ directory,
334
+ outreachId: 'noon',
335
+ cloudClient,
336
+ occurredAt: '2026-09-14T11:00:00.000Z',
337
+ });
338
+ assert.deepEqual(reconcileCalls, [{ dryRun: false, provenance: 'outreach-round-attach', streams: ['rounds'] }]);
339
+ assert.equal(result.counted, true);
340
+ assert.equal(result.roundId, 'cloud-round');
341
+ assert.equal(result.company, 'Noon');
342
+ });
343
+
344
+ test('unmigrated outreach backend is an empty verified set', async (t) => {
345
+ const directory = await mkdtemp(join(tmpdir(), 'outreach-round-unmigrated-'));
346
+ t.after(() => rm(directory, { recursive: true, force: true }));
347
+ const missingCapability = await loadSentVerifiedOutreach(directory, {
348
+ configured: async () => true,
349
+ status: async () => ({ configured: true, capabilities: ['application-accounting-v1'] }),
350
+ request: async () => {
351
+ throw new Error('Cloud state request failed (409): Outreach backend migration required');
352
+ },
353
+ });
354
+ assert.deepEqual(missingCapability, []);
355
+ const migrationConflict = await loadSentVerifiedOutreach(directory, {
356
+ configured: async () => true,
357
+ status: async () => ({ configured: true, capabilities: ['application-accounting-v1', 'outreach-tracking-v1'] }),
358
+ request: async () => {
359
+ throw new Error('Cloud state request failed (409): Outreach backend migration required');
360
+ },
361
+ });
362
+ assert.deepEqual(migrationConflict, []);
363
+ });
364
+
365
+ test('local outreach read failures stay visible to round status', async (t) => {
366
+ const directory = await mkdtemp(join(tmpdir(), 'outreach-round-locked-'));
367
+ t.after(() => rm(directory, { recursive: true, force: true }));
368
+ await writeFile(join(directory, 'outreach.lock'), 'stale-lock');
369
+ await assert.rejects(
370
+ () => loadSentVerifiedOutreach(directory, { configured: async () => false }),
371
+ /Outreach store is unavailable/,
372
+ );
373
+ });
374
+
375
+ test('delivery correction invalidates a counted confirmation; clear does not', () => {
376
+ const confirmation = { type: 'submission-confirmed', roundId: 'round-1', outreachId: 'noon', channel: OUTREACH_ROUND_CHANNEL, company: 'Noon' };
377
+ const failed = {
378
+ id: 'noon',
379
+ delivery: 'failed',
380
+ cleared: false,
381
+ attempts: [{ id: 'handoff-noon', purpose: 'initial', delivery: 'failed' }],
382
+ content: { assessment: { company: { name: 'Noon' }, role: 'Staff Product Engineer' } },
383
+ };
384
+ const failedCounts = projectOutreachRoundCounts({
385
+ applications: [],
386
+ delivery: deliveryProjection([], []),
387
+ roundEvents: [confirmation],
388
+ sentVerified: sentVerifiedFromOutreachItems([failed]),
389
+ invalidatedIds: invalidatedOutreachIds([failed]),
390
+ roundId: 'round-1',
391
+ });
392
+ assert.equal(failedCounts.outreachConfirmationCount, 0);
393
+ assert.equal(failedCounts.confirmedCount, 0);
394
+
395
+ const cleared = {
396
+ id: 'noon',
397
+ delivery: 'sent-verified',
398
+ cleared: true,
399
+ attempts: [{ id: 'handoff-noon', purpose: 'initial', delivery: 'sent-verified' }],
400
+ content: { assessment: { company: { name: 'Noon' }, role: 'Staff Product Engineer' } },
401
+ };
402
+ const clearedCounts = projectOutreachRoundCounts({
403
+ applications: [],
404
+ delivery: deliveryProjection([], []),
405
+ roundEvents: [confirmation],
406
+ sentVerified: sentVerifiedFromOutreachItems([cleared]),
407
+ invalidatedIds: invalidatedOutreachIds([cleared]),
408
+ roundId: 'round-1',
409
+ });
410
+ assert.equal(clearedCounts.outreachConfirmationCount, 1);
411
+ assert.equal(clearedCounts.confirmedCount, 1);
412
+ });
413
+
414
+ test('attached company identity stays on the confirmation event', () => {
415
+ const applications = [application('round-1', 'Acme')];
416
+ const delivery = deliveryProjection(applications, []);
417
+ const renamed = verifiedItem('acme', 'Acme Holdings');
418
+ const counts = projectOutreachRoundCounts({
419
+ applications,
420
+ delivery,
421
+ roundEvents: [{ type: 'submission-confirmed', roundId: 'round-1', outreachId: 'acme', channel: OUTREACH_ROUND_CHANNEL, company: 'Acme' }],
422
+ sentVerified: sentVerifiedFromOutreachItems([renamed]),
423
+ invalidatedIds: invalidatedOutreachIds([renamed]),
424
+ roundId: 'round-1',
425
+ });
426
+ assert.equal(counts.applyConfirmationCount, 1);
427
+ assert.equal(counts.outreachConfirmationCount, 0);
428
+ assert.equal(counts.confirmedCount, 1);
429
+ });
430
+
431
+ test('non-ASCII company names keep distinct count keys', () => {
432
+ assert.equal(companyCountKey('Made Card'), companyCountKey('MadeCard'));
433
+ assert.notEqual(companyCountKey('ソニー'), '');
434
+ assert.notEqual(companyCountKey('トヨタ'), '');
435
+ assert.notEqual(companyCountKey('ソニー'), companyCountKey('トヨタ'));
436
+ });
437
+
438
+ test('an earlier sent-verified attempt still qualifies after a later pending follow-up', () => {
439
+ const item = {
440
+ id: 'noon',
441
+ delivery: 'pending-handoff',
442
+ attempts: [
443
+ { id: 'handoff-noon', purpose: 'initial', delivery: 'sent-verified' },
444
+ { id: 'follow-noon', purpose: 'follow-up', delivery: 'pending-handoff' },
445
+ ],
446
+ content: { assessment: { company: { name: 'Noon' }, role: 'Staff Product Engineer' } },
447
+ };
448
+ const verified = sentVerifiedFromOutreachItems([item]);
449
+ assert.equal(verified.length, 1);
450
+ assert.equal(verified[0].id, 'noon');
451
+ });
452
+
453
+ test('confirm refuses a cached outreach snapshot when cloud is unavailable', async (t) => {
454
+ const directory = await mkdtemp(join(tmpdir(), 'outreach-round-no-cache-confirm-'));
455
+ t.after(() => rm(directory, { recursive: true, force: true }));
456
+ await writeFile(join(directory, 'rounds.ndjson'), `${JSON.stringify({ type: 'started', roundId: 'round-1', requestedCount: 30, occurredAt: '2026-09-14T10:00:00.000Z' })}\n`);
457
+ await writeFile(join(directory, 'outreach-cloud-cache.json'), JSON.stringify({
458
+ snapshot: { items: [verifiedItem('noon', 'Noon')] },
459
+ }));
460
+ const cloudClient = {
461
+ configured: async () => true,
462
+ status: async () => ({ configured: true, capabilities: ['outreach-tracking-v1'] }),
463
+ request: async () => {
464
+ throw new Error('Cloud state unavailable: network down');
465
+ },
466
+ reconcile: async () => {
467
+ throw new Error('Cloud state unavailable: network down');
468
+ },
469
+ appendRecord: async () => {
470
+ throw new Error('should not append from cache');
471
+ },
472
+ };
473
+ await assert.rejects(
474
+ () => confirmOutreachTowardRound({ directory, outreachId: 'noon', cloudClient }),
475
+ /authoritative state/,
476
+ );
477
+ const cachedStatus = await loadSentVerifiedOutreach(directory, cloudClient, { allowCache: true });
478
+ assert.equal(cachedStatus.length, 1);
479
+ });
480
+
481
+ test('round attachment failure does not disguise a committed send', async (t) => {
482
+ const { directory, cli } = await fixture(t, 'attachment-failed');
483
+ const { roundId } = cli(['round', 'start', '--stdin'], { requestedCount: 30 });
484
+ await writeFile(join(directory, 'rounds.ndjson'), `${JSON.stringify({ type: 'started', roundId, requestedCount: 30, occurredAt: '2026-09-14T10:00:00.000Z' })}\nnot-json\n`);
485
+ const recorded = recordVerifiedOutreach(cli, { id: 'noon', company: 'Noon', domain: 'noon.com' });
486
+ assert.equal(recorded.delivery, 'sent-verified');
487
+ assert.equal(recorded.roundConfirmation.counted, false);
488
+ assert.equal(recorded.roundConfirmation.reason, 'attachment-failed');
489
+ });
@@ -43,6 +43,25 @@ test('documents durable autonomy, resumable rounds, attention and friction contr
43
43
  assert.match(agentBox, /DISPLAY=:99/);
44
44
  });
45
45
 
46
+ test('documents flexible ledger check lookup as a separate process from outcome', async () => {
47
+ const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
48
+ const marketplaceSkill = await readFile(new URL('../../skills/job-application-agent/SKILL.md', import.meta.url), 'utf8');
49
+ const schemas = await readFile(new URL('../references/SCHEMAS.md', import.meta.url), 'utf8');
50
+
51
+ for (const copy of [skill, marketplaceSkill]) {
52
+ assert.match(copy, /two separate CLI processes/);
53
+ assert.match(copy, /company\+role/);
54
+ assert.match(copy, /possible duplicate/);
55
+ assert.match(copy, /never treat it as a hard already-applied/);
56
+ assert.match(copy, /look up the row with `ledger check` first/);
57
+ assert.match(copy, /pass the returned `match\.id` to `ledger outcome`/);
58
+ assert.match(copy, /If `match\.id` is absent, stop and ask/);
59
+ }
60
+ assert.match(schemas, /any one identifier set/);
61
+ assert.match(schemas, /Never promote a company\+role match to a hard already-applied/);
62
+ assert.match(schemas, /ledger add` still requires a real job URL/);
63
+ });
64
+
46
65
  test('documents Free.ai as optional LLM assist only', async () => {
47
66
  const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
48
67
  const freeAi = await readFile(new URL('../references/FREE_AI.md', import.meta.url), 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-application-agent",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
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",
@@ -44,7 +44,7 @@
44
44
  "check:native-artifacts": "node scripts/ci/validate-native-artifacts.mjs",
45
45
  "privacy-audit": "node --test job-application-agent/tests/privacy-audit.test.mjs job-application-agent/tests/outreach-cli.test.mjs",
46
46
  "smoke:package": "node scripts/smoke-package.mjs",
47
- "check": "node --check job-application-agent/scripts/job-application.mjs && node --check job-application-agent/scripts/attention-runner-poll.mjs && node --check job-application-agent/scripts/attention-resume-submit.mjs && node --check job-application-agent/scripts/session-binding.mjs && node --check job-application-agent/scripts/novnc-display-guard.mjs && node --check job-application-agent/scripts/ats/submit-adapters.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 && node --check job-application-agent/scripts/outreach-domain.mjs && node --check job-application-agent/scripts/outreach-store.mjs && node --check job-application-agent/scripts/outreach-cli.mjs && node --check state-worker/src/outreach.mjs && node --check job-application-agent/scripts/ats/answer-inject.mjs && node --check job-application-agent/scripts/attention-questions.mjs && node --check job-application-agent/scripts/captcha-vendor.mjs",
47
+ "check": "node --check job-application-agent/scripts/job-application.mjs && node --check job-application-agent/scripts/attention-runner-poll.mjs && node --check job-application-agent/scripts/attention-resume-submit.mjs && node --check job-application-agent/scripts/session-binding.mjs && node --check job-application-agent/scripts/novnc-display-guard.mjs && node --check job-application-agent/scripts/ats/submit-adapters.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 && node --check job-application-agent/scripts/outreach-domain.mjs && node --check job-application-agent/scripts/outreach-store.mjs && node --check job-application-agent/scripts/outreach-cli.mjs && node --check job-application-agent/scripts/outreach-round.mjs && node --check state-worker/src/outreach.mjs && node --check job-application-agent/scripts/ats/answer-inject.mjs && node --check job-application-agent/scripts/attention-questions.mjs && node --check job-application-agent/scripts/captcha-vendor.mjs",
48
48
  "prepack": "npm run check && npm test && npm run privacy-audit"
49
49
  },
50
50
  "engines": {