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,267 @@
1
+ import { appendFile, chmod, mkdir, open, readFile, unlink } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { deliveryProjection } from './application-accounting.mjs';
4
+ import { CloudStateClient, defaultCloudConfigPath } from './cloud-state-client.mjs';
5
+ import { OUTREACH_CAPABILITY, readOutreach } from './outreach-domain.mjs';
6
+ import { withLocalOutreach } from './outreach-store.mjs';
7
+
8
+ export const OUTREACH_ROUND_CHANNEL = 'outreach';
9
+
10
+ export function companyCountKey(value) {
11
+ return String(value ?? '').normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
12
+ }
13
+
14
+ export function hasActiveSentVerified(item) {
15
+ if (!item || item.cleared) return false;
16
+ const attempts = Array.isArray(item.attempts) ? item.attempts : [];
17
+ if (attempts.length > 0) return attempts.some((attempt) => attempt.delivery === 'sent-verified');
18
+ return item.delivery === 'sent-verified';
19
+ }
20
+
21
+ export function invalidatedOutreachIds(items = []) {
22
+ return items.filter((item) => item?.id && !item.cleared && !hasActiveSentVerified(item)).map((item) => item.id);
23
+ }
24
+
25
+ export function outreachConfirmationEvents(roundEvents, roundId) {
26
+ return roundEvents.filter((event) => event.type === 'submission-confirmed' && event.roundId === roundId && typeof event.outreachId === 'string' && event.outreachId && event.channel === OUTREACH_ROUND_CHANNEL);
27
+ }
28
+
29
+ export function countedApplyCompanies(applications, delivery) {
30
+ const companies = new Set();
31
+ applications.forEach((entry, index) => {
32
+ if (delivery.applications[index]?.counted) companies.add(companyCountKey(entry.company));
33
+ });
34
+ return companies;
35
+ }
36
+
37
+ export function sentVerifiedFromOutreachItems(items = []) {
38
+ return items.flatMap((item) => {
39
+ if (!hasActiveSentVerified(item)) return [];
40
+ const company = item.content?.assessment?.company?.name;
41
+ if (!company) return [];
42
+ return [{
43
+ id: item.id,
44
+ company,
45
+ role: item.content?.assessment?.role ?? null,
46
+ companyKey: companyCountKey(company),
47
+ }];
48
+ });
49
+ }
50
+
51
+ export function sentVerifiedFromOutreachState(state, now = new Date().toISOString()) {
52
+ if (!state?.opportunities) return [];
53
+ return sentVerifiedFromOutreachItems(Object.keys(state.opportunities).map((id) => readOutreach(state, 'show', { id }, now)));
54
+ }
55
+
56
+ export function resolveOutreachRoundId(roundEvents, { outreachId, roundId = null } = {}) {
57
+ const attached = roundEvents.find((event) => event.type === 'submission-confirmed' && event.outreachId === outreachId && event.channel === OUTREACH_ROUND_CHANNEL);
58
+ if (attached) {
59
+ if (roundId && attached.roundId !== roundId) {
60
+ throw new Error(`Outreach ${outreachId} is already attached to another round.`);
61
+ }
62
+ return attached.roundId;
63
+ }
64
+ if (roundId) return roundId;
65
+ const completed = new Set(roundEvents.filter((event) => event.type === 'completed').map((event) => event.roundId));
66
+ const open = roundEvents.filter((event) => event.type === 'started' && !completed.has(event.roundId));
67
+ return open.at(-1)?.roundId ?? null;
68
+ }
69
+
70
+ export function projectOutreachRoundCounts({ applications, delivery, roundEvents, sentVerified = [], roundId, invalidatedIds = [] }) {
71
+ const applyConfirmationCount = delivery.effectiveSubmissionCount;
72
+ const applyCompanies = countedApplyCompanies(applications, delivery);
73
+ const confirmations = outreachConfirmationEvents(roundEvents, roundId);
74
+ const invalidated = new Set(invalidatedIds);
75
+ const countedCompanies = new Set(applyCompanies);
76
+ const countedOutreachIds = [];
77
+ for (const event of confirmations) {
78
+ if (invalidated.has(event.outreachId)) continue;
79
+ const companyKey = companyCountKey(event.company);
80
+ if (countedCompanies.has(companyKey)) continue;
81
+ countedCompanies.add(companyKey);
82
+ countedOutreachIds.push(event.outreachId);
83
+ }
84
+ const outreachConfirmationCount = countedOutreachIds.length;
85
+ return {
86
+ applyConfirmationCount,
87
+ outreachConfirmationCount,
88
+ confirmedCount: applyConfirmationCount + outreachConfirmationCount,
89
+ countedOutreachIds,
90
+ };
91
+ }
92
+
93
+ function alreadyCountedOutreachCompany(confirmations, companyKey) {
94
+ return confirmations.some((event) => companyCountKey(event.company) === companyKey);
95
+ }
96
+
97
+ function confirmationItem(event) {
98
+ if (!event?.company) return undefined;
99
+ return { id: event.outreachId, company: event.company, companyKey: companyCountKey(event.company) };
100
+ }
101
+
102
+ function shouldCountOutreach({ sentVerified, applications, delivery, confirmations, outreachId }) {
103
+ const recorded = confirmations.find((event) => event.outreachId === outreachId);
104
+ if (recorded) {
105
+ return { count: false, reason: 'already-recorded', item: sentVerified.find((entry) => entry.id === outreachId) ?? confirmationItem(recorded) };
106
+ }
107
+ const item = sentVerified.find((entry) => entry.id === outreachId);
108
+ if (!item) return { count: false, reason: 'not-sent-verified' };
109
+ if (countedApplyCompanies(applications, delivery).has(item.companyKey)) return { count: false, reason: 'already-applied', item };
110
+ if (alreadyCountedOutreachCompany(confirmations, item.companyKey)) return { count: false, reason: 'already-counted-outreach', item };
111
+ return { count: true, item };
112
+ }
113
+
114
+ async function jsonLines(file) {
115
+ try {
116
+ return (await readFile(file, 'utf8')).split('\n').filter(Boolean).map((line, index) => {
117
+ try { return JSON.parse(line); } catch { throw new Error(`Invalid JSON on line ${index + 1} of ${basename(file)}.`); }
118
+ });
119
+ } catch (error) {
120
+ if (error.code === 'ENOENT') return [];
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ async function withFileLock(directory, name, action) {
126
+ await mkdir(directory, { recursive: true, mode: 0o700 });
127
+ await chmod(directory, 0o700);
128
+ const lockPath = join(directory, `.${name}.lock`);
129
+ let handle;
130
+ for (let attempt = 0; attempt < 100; attempt += 1) {
131
+ try {
132
+ handle = await open(lockPath, 'wx', 0o600);
133
+ break;
134
+ } catch (error) {
135
+ if (error.code !== 'EEXIST') throw error;
136
+ await new Promise((resolve) => setTimeout(resolve, 10));
137
+ }
138
+ }
139
+ if (!handle) throw new Error(`Could not acquire ${name} ledger lock.`);
140
+ try { return await action(); }
141
+ finally {
142
+ await handle.close();
143
+ await unlink(lockPath).catch((error) => { if (error.code !== 'ENOENT') throw error; });
144
+ }
145
+ }
146
+
147
+ async function snapshotItemsFromCache(directory) {
148
+ try {
149
+ const cache = JSON.parse(await readFile(join(directory, 'outreach-cloud-cache.json'), 'utf8'));
150
+ return Array.isArray(cache?.snapshot?.items) ? cache.snapshot.items : [];
151
+ } catch (error) {
152
+ if (error.code === 'ENOENT') return [];
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ function isOutreachMigrationRequired(error) {
158
+ return /\(409\): Outreach backend migration required/i.test(error.message) || /Outreach backend migration required/i.test(error.message);
159
+ }
160
+
161
+ function authoritativeSnapshotError(cause) {
162
+ const error = new Error('Outreach snapshot is unavailable; attachment requires authoritative state.');
163
+ if (cause) error.cause = cause;
164
+ return error;
165
+ }
166
+
167
+ export async function loadOutreachSnapshotItems(directory, cloudClient, { allowCache = true } = {}) {
168
+ const cloud = cloudClient ?? new CloudStateClient({ stateDir: directory, configPath: process.env.JOB_APPLICATION_AGENT_CLOUD_CONFIG ?? defaultCloudConfigPath() });
169
+ if (await cloud.configured()) {
170
+ try {
171
+ if (typeof cloud.status === 'function') {
172
+ const status = await cloud.status();
173
+ if (!status.capabilities?.includes(OUTREACH_CAPABILITY)) return [];
174
+ }
175
+ const snapshot = await (await cloud.request('/v2/outreach/snapshot')).json();
176
+ return Array.isArray(snapshot.items) ? snapshot.items : [];
177
+ } catch (error) {
178
+ if (isOutreachMigrationRequired(error)) return [];
179
+ if (!/^Cloud state unavailable:/.test(error.message)) throw error;
180
+ if (!allowCache) throw authoritativeSnapshotError(error);
181
+ }
182
+ if (!allowCache) throw authoritativeSnapshotError();
183
+ return snapshotItemsFromCache(directory);
184
+ }
185
+ try {
186
+ return await withLocalOutreach(directory, (state) => ({
187
+ result: Object.keys(state.opportunities).map((id) => readOutreach(state, 'show', { id })),
188
+ }));
189
+ } catch (error) {
190
+ if (error.code === 'ENOENT') return [];
191
+ const wrapped = new Error(`Outreach store is unavailable: ${error.message}`);
192
+ wrapped.cause = error;
193
+ throw wrapped;
194
+ }
195
+ }
196
+
197
+ export async function loadSentVerifiedOutreach(directory, cloudClient, options) {
198
+ return sentVerifiedFromOutreachItems(await loadOutreachSnapshotItems(directory, cloudClient, options));
199
+ }
200
+
201
+ export async function loadOutreachRoundView(directory, cloudClient, options) {
202
+ const items = await loadOutreachSnapshotItems(directory, cloudClient, options);
203
+ return {
204
+ sentVerified: sentVerifiedFromOutreachItems(items),
205
+ invalidatedIds: invalidatedOutreachIds(items),
206
+ };
207
+ }
208
+
209
+ export async function confirmOutreachTowardRound({
210
+ directory,
211
+ outreachId,
212
+ roundId = null,
213
+ cloudClient = null,
214
+ requireRoundId = false,
215
+ occurredAt = new Date().toISOString(),
216
+ } = {}) {
217
+ const id = String(outreachId ?? '').trim();
218
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$/.test(id)) throw new Error('Invalid opaque outreach ID.');
219
+ const cloud = cloudClient ?? new CloudStateClient({ stateDir: directory, configPath: process.env.JOB_APPLICATION_AGENT_CLOUD_CONFIG ?? defaultCloudConfigPath() });
220
+ return withFileLock(directory, 'rounds', async () => {
221
+ if (await cloud.configured()) {
222
+ try {
223
+ await cloud.reconcile({ dryRun: false, provenance: 'outreach-round-attach', streams: ['rounds'] });
224
+ } catch (error) {
225
+ if (!/^Cloud state unavailable:/.test(error.message)) throw error;
226
+ }
227
+ }
228
+ const roundEvents = await jsonLines(join(directory, 'rounds.ndjson'));
229
+ const targetRoundId = resolveOutreachRoundId(roundEvents, { outreachId: id, roundId });
230
+ if (!targetRoundId) return { counted: false, reason: requireRoundId ? 'round-not-found' : 'no-active-round', outreachId: id, roundId: null };
231
+ const started = roundEvents.find((event) => event.type === 'started' && event.roundId === targetRoundId);
232
+ if (!started) throw new Error('Application round was not found.');
233
+ if (roundEvents.some((event) => event.type === 'completed' && event.roundId === targetRoundId)) {
234
+ const existing = outreachConfirmationEvents(roundEvents, targetRoundId).some((event) => event.outreachId === id);
235
+ return { counted: existing, reason: existing ? 'already-recorded' : 'round-completed', outreachId: id, roundId: targetRoundId };
236
+ }
237
+ const applications = (await jsonLines(join(directory, 'applications.ndjson'))).filter((entry) => entry.roundId === targetRoundId && entry.status === 'submitted');
238
+ const delivery = deliveryProjection(applications, await jsonLines(join(directory, 'delivery.ndjson')));
239
+ const sentVerified = await loadSentVerifiedOutreach(directory, cloud, { allowCache: false });
240
+ const confirmations = outreachConfirmationEvents(roundEvents, targetRoundId);
241
+ const decision = shouldCountOutreach({ sentVerified, applications, delivery, confirmations, outreachId: id });
242
+ if (!decision.count) {
243
+ return { counted: decision.reason === 'already-recorded', reason: decision.reason, outreachId: id, roundId: targetRoundId, company: decision.item?.company ?? null };
244
+ }
245
+ const event = {
246
+ type: 'submission-confirmed',
247
+ roundId: targetRoundId,
248
+ outreachId: id,
249
+ channel: OUTREACH_ROUND_CHANNEL,
250
+ company: decision.item.company,
251
+ ...(decision.item.role ? { role: decision.item.role } : {}),
252
+ occurredAt,
253
+ };
254
+ const file = join(directory, 'rounds.ndjson');
255
+ await appendFile(file, `${JSON.stringify(event)}\n`, { mode: 0o600 });
256
+ await chmod(file, 0o600);
257
+ if (await cloud.configured()) {
258
+ await cloud.appendRecord('rounds', event, {
259
+ recordKey: targetRoundId,
260
+ idempotencyKey: `round-confirmation:${targetRoundId}:outreach:${id}`,
261
+ occurredAt,
262
+ queueOnFailure: true,
263
+ });
264
+ }
265
+ return { counted: true, reason: 'recorded', outreachId: id, roundId: targetRoundId, company: decision.item.company };
266
+ });
267
+ }
@@ -1 +1 @@
1
- export const SKILL_VERSION = '3.7.0';
1
+ export const SKILL_VERSION = '3.7.2';
@@ -86,6 +86,18 @@ sqliteTest('reconcile includes the owner-only discovery review ledger', async ()
86
86
  assert.deepEqual((await ctx.client.listStream('discovery'))[0].value, lead);
87
87
  });
88
88
 
89
+ sqliteTest('targeted reconcile only syncs the requested stream', async () => {
90
+ const ctx = await setup();
91
+ await writeFile(join(ctx.stateDir, 'applications.ndjson'), `${JSON.stringify({ id: 'app-1', company: 'A', submittedAt: '2026-01-01T00:00:00.000Z' })}\n`);
92
+ await writeFile(join(ctx.stateDir, 'rounds.ndjson'), `${JSON.stringify({ type: 'started', roundId: 'round-1', requestedCount: 1, occurredAt: '2026-01-01T00:00:00.000Z' })}\n`);
93
+ const report = await ctx.client.reconcile({ dryRun: false, streams: ['rounds'] });
94
+ assert.equal(report.imported, 1);
95
+ assert.ok(report.streams.rounds);
96
+ assert.equal(report.streams.applications, undefined);
97
+ assert.equal((await ctx.bindings.DB.prepare("SELECT COUNT(*) AS count FROM records WHERE stream = 'applications'").first()).count, 0);
98
+ assert.equal((await ctx.bindings.DB.prepare("SELECT COUNT(*) AS count FROM records WHERE stream = 'rounds'").first()).count, 1);
99
+ });
100
+
89
101
  sqliteTest('resume download verifies checksum and creates a private path cache', async () => {
90
102
  const ctx = await setup();
91
103
  const bytes = Buffer.from('%PDF-1.7\ncloud-resume-fixture');
@@ -7,7 +7,7 @@ import test from 'node:test';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createServer } from 'node:http';
9
9
 
10
- import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryErrorCode, telemetryJobAssessed, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
10
+ import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryErrorCode, telemetryJobAssessed, validateLedgerCheckCandidate, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
11
11
 
12
12
  const target = {
13
13
  name: 'Test Candidate',
@@ -347,6 +347,164 @@ test('warns on same-company role matches and serializes concurrent duplicate sub
347
347
  assert.equal(possible.possibleDuplicate, true);
348
348
  });
349
349
 
350
+ test('accepts any one ledger check identifier set and never hard-matches company+role', () => {
351
+ assert.deepEqual(validateLedgerCheckCandidate({ url: 'https://jobs.example.com/123' }), { url: 'https://jobs.example.com/123' });
352
+ assert.deepEqual(validateLedgerCheckCandidate({ id: 'example-role-1' }), { id: 'example-role-1' });
353
+ assert.deepEqual(validateLedgerCheckCandidate({ employerJobId: 'example:123', company: 'Example' }), {
354
+ employerJobId: 'example:123', company: 'Example',
355
+ });
356
+ assert.deepEqual(validateLedgerCheckCandidate({ company: 'Example', role: 'Senior Product Engineer' }), {
357
+ company: 'Example', role: 'Senior Product Engineer',
358
+ });
359
+ const identifierError = /ledger check requires at least one identifier set: url, id, employerJobId\+company, or company\+role/;
360
+ assert.throws(() => validateLedgerCheckCandidate({}), identifierError);
361
+ assert.throws(() => validateLedgerCheckCandidate({ company: 'Example' }), identifierError);
362
+ assert.throws(() => validateLedgerCheckCandidate({ role: 'Senior Product Engineer' }), identifierError);
363
+ assert.throws(() => validateLedgerCheckCandidate({ employerJobId: 'example:123' }), identifierError);
364
+ try {
365
+ validateLedgerCheckCandidate({});
366
+ } catch (error) {
367
+ assert.match(error.message, identifierError);
368
+ assert.doesNotMatch(error.message, /candidate\.url must/);
369
+ }
370
+ assert.throws(() => validateLedgerCheckCandidate({ url: 'not-a-url' }), /Invalid URL|invalid/i);
371
+ assert.throws(() => validateLedgerCheckCandidate({ id: 'example-role-1', url: 123 }), /candidate\.url must/);
372
+ assert.throws(() => validateLedgerCheckCandidate({ id: 'example-role-1', url: null }), /candidate\.url must/);
373
+ });
374
+
375
+ test('looks up ledger rows without requiring a URL on check', async (t) => {
376
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-check-lookup-'));
377
+ t.after(() => rm(directory, { recursive: true, force: true }));
378
+ const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
379
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
380
+ const storedUrl = 'https://jobs.example.com/123';
381
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
382
+ id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: storedUrl, source: 'company', score: 88,
383
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
384
+ })}\n`);
385
+ const env = isolatedCliEnv(directory);
386
+
387
+ const byUrl = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
388
+ input: JSON.stringify({ url: 'https://jobs.example.com/123?utm_source=board' }), env, encoding: 'utf8',
389
+ }));
390
+ assert.equal(byUrl.duplicate, true);
391
+ assert.equal(byUrl.reason, 'url');
392
+
393
+ const byId = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
394
+ input: JSON.stringify({ id: 'example-role-1' }), env, encoding: 'utf8',
395
+ }));
396
+ assert.equal(byId.duplicate, true);
397
+ assert.equal(byId.possibleDuplicate, false);
398
+ assert.equal(byId.reason, 'id');
399
+ assert.equal(byId.match.url, storedUrl);
400
+
401
+ const byEmployerJob = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
402
+ input: JSON.stringify({ employerJobId: 'example:123', company: 'Example' }), env, encoding: 'utf8',
403
+ }));
404
+ assert.equal(byEmployerJob.duplicate, true);
405
+ assert.equal(byEmployerJob.possibleDuplicate, false);
406
+ assert.equal(byEmployerJob.reason, 'employer-job-id');
407
+
408
+ const newerUrl = 'https://jobs.example.com/123-new';
409
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
410
+ id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: storedUrl, source: 'company', score: 88,
411
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
412
+ })}\n${JSON.stringify({
413
+ id: 'example-role-1b', company: 'Example', role: 'Senior Product Engineer', url: newerUrl, source: 'company', score: 88,
414
+ status: 'submitted', submittedAt: '2026-02-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
415
+ })}\n`);
416
+
417
+ const byCompanyRole = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
418
+ input: JSON.stringify({ company: 'Example', role: 'Senior Product Engineer' }), env, encoding: 'utf8',
419
+ }));
420
+ assert.equal(byCompanyRole.duplicate, false);
421
+ assert.equal(byCompanyRole.possibleDuplicate, true);
422
+ assert.equal(byCompanyRole.reason, 'company-role');
423
+ assert.equal(byCompanyRole.match.url, newerUrl);
424
+ assert.equal(byCompanyRole.match.id, 'example-role-1b');
425
+
426
+ const seniorUrl = 'https://jobs.example.com/senior';
427
+ const staffUrl = 'https://jobs.example.com/staff';
428
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
429
+ id: 'example-senior', company: 'Example', role: 'Senior Software Engineer', url: seniorUrl, source: 'company', score: 88,
430
+ status: 'submitted', submittedAt: '2026-01-10T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
431
+ })}\n${JSON.stringify({
432
+ id: 'example-staff', company: 'Example', role: 'Staff Software Engineer', url: staffUrl, source: 'company', score: 90,
433
+ status: 'submitted', submittedAt: '2026-02-10T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
434
+ })}\n`);
435
+ const exactSenior = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
436
+ input: JSON.stringify({ company: 'Example', role: 'Senior Software Engineer' }), env, encoding: 'utf8',
437
+ }));
438
+ assert.equal(exactSenior.possibleDuplicate, true);
439
+ assert.equal(exactSenior.match.id, 'example-senior');
440
+ assert.equal(exactSenior.match.url, seniorUrl);
441
+
442
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
443
+ id: 'example-backend', company: 'Example', role: 'Backend Engineer', url: 'https://jobs.example.com/backend', source: 'company', score: 84,
444
+ status: 'submitted', submittedAt: '2026-01-05T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
445
+ })}\n${JSON.stringify({
446
+ id: 'example-frontend', company: 'Example', role: 'Frontend Engineer', url: 'https://jobs.example.com/frontend', source: 'company', score: 85,
447
+ status: 'submitted', submittedAt: '2026-03-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
448
+ })}\n`);
449
+ const ambiguousEngineer = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
450
+ input: JSON.stringify({ company: 'Example', role: 'Engineer' }), env, encoding: 'utf8',
451
+ }));
452
+ assert.equal(ambiguousEngineer.duplicate, false);
453
+ assert.equal(ambiguousEngineer.possibleDuplicate, true);
454
+ assert.equal(ambiguousEngineer.reason, 'company-role');
455
+ assert.equal(ambiguousEngineer.match, null);
456
+
457
+ const identifierError = /ledger check requires at least one identifier set: url, id, employerJobId\+company, or company\+role/;
458
+ for (const input of [{}, { company: 'Example' }, { role: 'Senior Product Engineer' }]) {
459
+ const failed = await runCli(script, ['ledger', 'check', '--stdin'], input, env);
460
+ assert.equal(failed.code, 1);
461
+ assert.match(failed.stderr, identifierError);
462
+ assert.doesNotMatch(failed.stderr, /candidate\.url must/);
463
+ }
464
+
465
+ const malformedUrl = await runCli(script, ['ledger', 'check', '--stdin'], { id: 'example-role-1', url: 123 }, env);
466
+ assert.equal(malformedUrl.code, 1);
467
+ assert.match(malformedUrl.stderr, /candidate\.url must/);
468
+
469
+ assert.equal(byId.sameCompany, true);
470
+ assert.equal(byId.companyApplications.length, 1);
471
+ assert.equal(byId.companyApplications[0].id, 'example-role-1');
472
+
473
+ const invalidUrl = await runCli(script, ['ledger', 'check', '--stdin'], { url: 'not-a-url' }, env);
474
+ assert.equal(invalidUrl.code, 1);
475
+ assert.match(invalidUrl.stderr, /Invalid URL|invalid/i);
476
+
477
+ const missingAddUrl = await runCli(script, ['ledger', 'add', '--stdin'], {
478
+ id: 'example-role-2', company: 'Example', role: 'Staff Engineer', source: 'company', score: 80,
479
+ status: 'submitted', submittedAt: '2026-01-16T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
480
+ }, env);
481
+ assert.equal(missingAddUrl.code, 1);
482
+ assert.match(missingAddUrl.stderr, /entry\.url must/);
483
+ });
484
+
485
+ test('records mail and looks up company+role in separate CLI processes', async (t) => {
486
+ const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcome-then-check-'));
487
+ t.after(() => rm(directory, { recursive: true, force: true }));
488
+ const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
489
+ await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
490
+ const storedUrl = 'https://jobs.example.com/apollo-1';
491
+ await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
492
+ id: 'apollo-role-1', company: 'Apollo', role: 'Staff Engineer', url: storedUrl, source: 'company', score: 90,
493
+ status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
494
+ })}\n`);
495
+ const env = isolatedCliEnv(directory);
496
+ const outcome = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], {
497
+ input: JSON.stringify({ id: 'apollo-role-1', status: 'rejected', occurredAt: '2026-01-20T09:00:00Z' }), env, encoding: 'utf8',
498
+ }));
499
+ assert.equal(outcome.recorded, true);
500
+ const check = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
501
+ input: JSON.stringify({ company: 'Apollo', role: 'Staff Engineer' }), env, encoding: 'utf8',
502
+ }));
503
+ assert.equal(check.duplicate, false);
504
+ assert.equal(check.possibleDuplicate, true);
505
+ assert.equal(check.match.url, storedUrl);
506
+ });
507
+
350
508
  test('records structured outcomes idempotently without duplicate rows', async (t) => {
351
509
  const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcomes-'));
352
510
  t.after(() => rm(directory, { recursive: true, force: true }));