job-application-agent 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/job-application-agent/SKILL.md +29 -19
- package/job-application-agent/references/ANALYTICS.md +4 -0
- package/job-application-agent/references/RUNS.md +2 -1
- package/job-application-agent/references/SCHEMAS.md +8 -3
- package/job-application-agent/references/SOURCES.json +156 -0
- package/job-application-agent/references/SOURCES.md +73 -0
- package/job-application-agent/scripts/job-application.mjs +286 -23
- package/job-application-agent/scripts/source-community-client.mjs +338 -0
- package/job-application-agent/scripts/source-community-schema.mjs +320 -0
- package/job-application-agent/scripts/telemetry-client.mjs +3 -1
- package/job-application-agent/scripts/version.mjs +1 -0
- package/job-application-agent/tests/job-application.test.mjs +12 -9
- package/job-application-agent/tests/privacy-audit.test.mjs +39 -0
- package/job-application-agent/tests/source-community-client.test.mjs +351 -0
- package/job-application-agent/tests/source-community-schema.test.mjs +270 -0
- package/job-application-agent/tests/telemetry-client.test.mjs +1 -1
- package/job-application-agent/tests/version.test.mjs +12 -0
- package/job-application-agent/tests/workflow-state.test.mjs +506 -7
- package/package.json +7 -3
|
@@ -2,8 +2,10 @@ import { chmod, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/prom
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { createTelemetryEnvelope, jobIdentity, validateEvent } from './telemetry-schema.mjs';
|
|
5
|
+
import { SKILL_VERSION } from './version.mjs';
|
|
6
|
+
|
|
7
|
+
export { SKILL_VERSION };
|
|
5
8
|
|
|
6
|
-
export const SKILL_VERSION = '1.2.1';
|
|
7
9
|
export const DEFAULT_TELEMETRY_ENDPOINT = process.env.JOB_APPLICATION_AGENT_TELEMETRY_URL ?? 'https://job-application-agent-telemetry.varora1406.workers.dev';
|
|
8
10
|
export const TELEMETRY_NOTICE = 'Anonymous usage analytics are enabled by default. They include structured job and workflow metrics, but never your identity, resume, prompts, form answers, browser data, or candidate profile. Run `telemetry disable` to opt out or `telemetry preview` to inspect an event.\n';
|
|
9
11
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SKILL_VERSION = '3.2.0';
|
|
@@ -4,6 +4,7 @@ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryJobAssessed, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
|
|
9
10
|
|
|
@@ -68,7 +69,7 @@ test('validates a candidate-defined target profile', () => {
|
|
|
68
69
|
test('returns the canonical resume path for direct browser uploads', async (t) => {
|
|
69
70
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-resume-path-'));
|
|
70
71
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
71
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
72
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
72
73
|
const resume = join(directory, 'resume.pdf');
|
|
73
74
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
74
75
|
await writeFile(resume, '%PDF-1.7\ncanonical resume fixture');
|
|
@@ -107,6 +108,7 @@ test('scores a matching role from candidate preferences', () => {
|
|
|
107
108
|
assert.equal(result.autoEligible, true);
|
|
108
109
|
assert.equal(result.mustHaveCoverage, 83);
|
|
109
110
|
assert.ok(result.score >= 80);
|
|
111
|
+
assert.equal(scoreJob({ ...matchingJob, discoverySourceId: 'community-abcdef1234567890' }, target).decision, 'review');
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
test('applies posting, eligibility, work-mode, seniority and evidence gates before auto-submit', () => {
|
|
@@ -238,7 +240,7 @@ test('requires review at each ten confirmed submissions', () => {
|
|
|
238
240
|
test('deduplicates ledger entries by normalized URL', async (t) => {
|
|
239
241
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-'));
|
|
240
242
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
241
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
243
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
242
244
|
const entry = {
|
|
243
245
|
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: {},
|
|
244
246
|
telemetry: { durationBucket: '5-15m', fieldsFilled: 14, shortAnswerCount: 2, resumeUploaded: true },
|
|
@@ -253,13 +255,13 @@ test('deduplicates ledger entries by normalized URL', async (t) => {
|
|
|
253
255
|
}));
|
|
254
256
|
assert.equal(relabelled.duplicate, true);
|
|
255
257
|
assert.equal((await readFile(join(directory, 'applications.ndjson'), 'utf8')).includes('telemetry'), false);
|
|
256
|
-
assert.equal((await stat(join(directory, 'applications.ndjson'))).mode & 0o777, 0o600);
|
|
258
|
+
if (process.platform !== 'win32') assert.equal((await stat(join(directory, 'applications.ndjson'))).mode & 0o777, 0o600);
|
|
257
259
|
});
|
|
258
260
|
|
|
259
261
|
test('warns on same-company role matches and serializes concurrent duplicate submissions', async (t) => {
|
|
260
262
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-dedup-'));
|
|
261
263
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
262
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
264
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
263
265
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
264
266
|
const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
|
|
265
267
|
const base = {
|
|
@@ -283,7 +285,7 @@ test('warns on same-company role matches and serializes concurrent duplicate sub
|
|
|
283
285
|
test('records structured outcomes idempotently without duplicate rows', async (t) => {
|
|
284
286
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcomes-'));
|
|
285
287
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
286
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
288
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
287
289
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
288
290
|
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
289
291
|
id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
|
|
@@ -306,7 +308,7 @@ test('records structured outcomes idempotently without duplicate rows', async (t
|
|
|
306
308
|
test('records bounded interview quality and failure-point enrichment idempotently', async (t) => {
|
|
307
309
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-interview-quality-'));
|
|
308
310
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
309
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
311
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
310
312
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
311
313
|
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
312
314
|
id: 'example-role-1', company: 'Example', role: 'Staff Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 91,
|
|
@@ -391,7 +393,7 @@ test('preserves interview quality after a later final outcome', () => {
|
|
|
391
393
|
test('acknowledges a generated review only through the explicit CLI command', async (t) => {
|
|
392
394
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-review-'));
|
|
393
395
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
394
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
396
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
395
397
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
396
398
|
const entries = Array.from({ length: 10 }, (_, index) => ({
|
|
397
399
|
id: `role-${index}`, company: `Company ${index}`, role: 'Senior Engineer', url: `https://jobs.example.com/${index}`,
|
|
@@ -414,6 +416,7 @@ test('maps commands and durations to bounded telemetry categories', () => {
|
|
|
414
416
|
assert.equal(commandCategory(['ledger', 'add', '--stdin']), 'apply');
|
|
415
417
|
assert.equal(commandCategory(['ledger', 'outcome', '--stdin']), 'outcome');
|
|
416
418
|
assert.equal(commandCategory(['score', '--stdin']), 'assess');
|
|
419
|
+
assert.equal(commandCategory(['sources', 'list']), 'search');
|
|
417
420
|
assert.equal(durationBucket(700), 'under-1s');
|
|
418
421
|
assert.equal(durationBucket(70_000), '1-2m');
|
|
419
422
|
assert.equal(durationBucket(2_000_000), '15m-plus');
|
|
@@ -436,11 +439,11 @@ test('builds a structured assessment event without description or candidate prof
|
|
|
436
439
|
test('telemetry CLI controls are private and reset removes anonymous credentials', async (t) => {
|
|
437
440
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-telemetry-'));
|
|
438
441
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
439
|
-
const script = new URL('../scripts/job-application.mjs', import.meta.url)
|
|
442
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
440
443
|
const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory, JOB_APPLICATION_AGENT_TELEMETRY_URL: 'https://relay.invalid' };
|
|
441
444
|
const disabled = JSON.parse(execFileSync(process.execPath, [script, 'telemetry', 'disable'], { env, encoding: 'utf8' }));
|
|
442
445
|
assert.equal(disabled.enabled, false);
|
|
443
446
|
const reset = JSON.parse(execFileSync(process.execPath, [script, 'telemetry', 'reset'], { env, encoding: 'utf8' }));
|
|
444
447
|
assert.equal(reset.hasInstallationId, false);
|
|
445
|
-
assert.equal((await stat(join(directory, 'telemetry.json'))).mode & 0o777, 0o600);
|
|
448
|
+
if (process.platform !== 'win32') assert.equal((await stat(join(directory, 'telemetry.json'))).mode & 0o777, 0o600);
|
|
446
449
|
});
|
|
@@ -3,6 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
|
|
4
4
|
import { prepareTelemetryInput } from '../scripts/telemetry-client.mjs';
|
|
5
5
|
import { validateEvent } from '../scripts/telemetry-schema.mjs';
|
|
6
|
+
import { createCommunityJobContributionEnvelope, normalizeCommunityJob, normalizeCommunitySource } from '../scripts/source-community-schema.mjs';
|
|
6
7
|
|
|
7
8
|
const forbiddenProperties = [
|
|
8
9
|
'name', 'email', 'phone', 'exactAddress', 'linkedin', 'github', 'portfolio', 'candidateLocation',
|
|
@@ -40,3 +41,41 @@ test('privacy audit strips the full query and fragment before job URL hashing',
|
|
|
40
41
|
assert.equal(serialized.includes('secret'), false);
|
|
41
42
|
assert.equal(serialized.includes('jobs.example.com/role/123'), false);
|
|
42
43
|
});
|
|
44
|
+
|
|
45
|
+
test('privacy audit strips identity-bearing source parameters and rejects personal source metadata', () => {
|
|
46
|
+
const source = normalizeCommunitySource({
|
|
47
|
+
name: 'Example Engineering Board',
|
|
48
|
+
baseUrl: 'https://jobs.example.org/openings/engineering?email=candidate@example.com&token=secret#private',
|
|
49
|
+
kind: 'job-board',
|
|
50
|
+
regions: ['global'],
|
|
51
|
+
roleFamilies: ['engineering'],
|
|
52
|
+
requiresSession: false,
|
|
53
|
+
});
|
|
54
|
+
assert.equal(source.baseUrl, 'https://jobs.example.org/openings/engineering');
|
|
55
|
+
assert.equal(JSON.stringify(source).includes('candidate@example.com'), false);
|
|
56
|
+
assert.throws(() => normalizeCommunitySource({ ...source, name: 'candidate@example.com' }), /identity/i);
|
|
57
|
+
assert.throws(() => normalizeCommunitySource({ ...source, baseUrl: 'https://linkedin.com/in/candidate' }), /profile or personal/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('privacy audit permits only sanitized public fields in community jobs', () => {
|
|
61
|
+
const job = normalizeCommunityJob({
|
|
62
|
+
url: 'https://jobs.ashbyhq.com/example/12345678-1234-4123-8123-123456789abc?email=candidate@example.com&token=secret#private',
|
|
63
|
+
company: 'Example',
|
|
64
|
+
role: 'Senior Product Engineer',
|
|
65
|
+
applicationChannel: 'ashby',
|
|
66
|
+
discoverySource: 'job-board',
|
|
67
|
+
});
|
|
68
|
+
const envelope = createCommunityJobContributionEnvelope({
|
|
69
|
+
installationId: '11111111-1111-4111-8111-111111111111',
|
|
70
|
+
token: 'anonymous-relay-token',
|
|
71
|
+
skillVersion: '3.2.0',
|
|
72
|
+
job,
|
|
73
|
+
});
|
|
74
|
+
const serialized = JSON.stringify(envelope.job);
|
|
75
|
+
assert.equal(serialized.includes('candidate@example.com'), false);
|
|
76
|
+
assert.equal(serialized.includes('secret'), false);
|
|
77
|
+
assert.deepEqual(Object.keys(envelope.job).sort(), ['applicationChannel', 'company', 'discoverySource', 'providerUrl', 'role', 'url']);
|
|
78
|
+
assert.throws(() => normalizeCommunityJob({ ...job, answers: { private: true } }), /unknown/i);
|
|
79
|
+
assert.throws(() => normalizeCommunityJob({ ...job, company: 'candidate@example.com' }), /identity/i);
|
|
80
|
+
assert.throws(() => normalizeCommunityJob({ ...job, url: 'https://linkedin.com/in/candidate' }), /personal/i);
|
|
81
|
+
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, readFile, 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 { SourceCommunityClient } from '../scripts/source-community-client.mjs';
|
|
8
|
+
|
|
9
|
+
const source = {
|
|
10
|
+
name: 'Example Engineering Board',
|
|
11
|
+
baseUrl: 'https://jobs.example.org/openings/engineering?ref=candidate@example.com#openings',
|
|
12
|
+
kind: 'job-board',
|
|
13
|
+
regions: ['global', 'remote'],
|
|
14
|
+
roleFamilies: ['engineering'],
|
|
15
|
+
requiresSession: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const job = {
|
|
19
|
+
url: 'https://jobs.ashbyhq.com/example/12345678-1234-4123-8123-123456789abc?ref=candidate@example.com#apply',
|
|
20
|
+
company: 'Example',
|
|
21
|
+
role: 'Senior Product Engineer',
|
|
22
|
+
applicationChannel: 'ashby',
|
|
23
|
+
discoverySource: 'job-board',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function relay() {
|
|
27
|
+
const requests = [];
|
|
28
|
+
const community = [{
|
|
29
|
+
sourceId: 'community-abcdef1234567890',
|
|
30
|
+
name: 'Example Engineering Board',
|
|
31
|
+
baseUrl: 'https://jobs.example.org/openings/engineering',
|
|
32
|
+
kind: 'job-board',
|
|
33
|
+
regions: ['global', 'remote'],
|
|
34
|
+
roleFamilies: ['engineering'],
|
|
35
|
+
requiresSession: false,
|
|
36
|
+
registryStatus: 'community-reviewed',
|
|
37
|
+
contributionCount: 2,
|
|
38
|
+
}];
|
|
39
|
+
const jobs = [{
|
|
40
|
+
jobId: 'community-job-abcdef1234567890',
|
|
41
|
+
url: 'https://jobs.ashbyhq.com/example/12345678-1234-4123-8123-123456789abc',
|
|
42
|
+
company: 'Example',
|
|
43
|
+
role: 'Senior Product Engineer',
|
|
44
|
+
applicationChannel: 'ashby',
|
|
45
|
+
discoverySource: 'job-board',
|
|
46
|
+
providerUrl: 'https://jobs.ashbyhq.com/example',
|
|
47
|
+
firstSeenAt: '2026-08-24T00:00:00.000Z',
|
|
48
|
+
lastSeenAt: '2026-08-24T00:00:00.000Z',
|
|
49
|
+
contributionCount: 2,
|
|
50
|
+
}];
|
|
51
|
+
const fetch = async (url, options = {}) => {
|
|
52
|
+
requests.push({ url, options, body: options.body ? JSON.parse(options.body) : null });
|
|
53
|
+
if (url.endsWith('/v1/install')) return Response.json({ installationId: '11111111-1111-4111-8111-111111111111', token: 'source-token', expiresAt: '2099-01-01T00:00:00.000Z' }, { status: 201 });
|
|
54
|
+
if (url.endsWith('/v1/sources') && options.method === 'POST') return Response.json({ accepted: true, sourceId: 'community-abcdef1234567890', publicationStatus: 'pending', uniqueContributors: 1 }, { status: 202 });
|
|
55
|
+
if (url.endsWith('/v1/jobs') && options.method === 'POST') return Response.json({ accepted: true, jobId: 'community-job-abcdef1234567890', publicationStatus: 'pending', contributionCount: 2 }, { status: 202 });
|
|
56
|
+
if (url.includes('/v1/jobs')) return Response.json({ version: 1, jobs, nextCursor: 'next-page' });
|
|
57
|
+
return Response.json({ version: 1, sources: community });
|
|
58
|
+
};
|
|
59
|
+
return { fetch, requests };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('source sharing is enabled by default, disclosed, sanitized, and sent immediately', async (t) => {
|
|
63
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-default-'));
|
|
64
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
65
|
+
const network = relay();
|
|
66
|
+
let notice = '';
|
|
67
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: (value) => { notice += value; } });
|
|
68
|
+
|
|
69
|
+
const result = await client.contribute(source);
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(result, { shared: true, sourceId: 'community-abcdef1234567890', publicationStatus: 'pending', uniqueContributors: 1 });
|
|
72
|
+
assert.match(notice, /community sharing is enabled by default/i);
|
|
73
|
+
assert.equal(network.requests.length, 2);
|
|
74
|
+
assert.equal(network.requests[1].url, 'https://relay.example.com/v1/sources');
|
|
75
|
+
assert.equal(network.requests[1].body.source.baseUrl, 'https://jobs.example.org/openings/engineering');
|
|
76
|
+
assert.equal(JSON.stringify(network.requests[1].body).includes('candidate@example.com'), false);
|
|
77
|
+
const stored = JSON.parse(await readFile(join(directory, 'source-sharing.json'), 'utf8'));
|
|
78
|
+
assert.equal(stored.enabled, true);
|
|
79
|
+
assert.equal(stored.disclosed, true);
|
|
80
|
+
assert.equal(stored.jobSharingDisclosed, true);
|
|
81
|
+
if (process.platform !== 'win32') assert.equal((await stat(join(directory, 'source-sharing.json'))).mode & 0o777, 0o600);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('existing source-sharing users receive one command to opt out before job backfill', async (t) => {
|
|
85
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-upgrade-disclosure-'));
|
|
86
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
87
|
+
await writeFile(join(directory, 'source-sharing.json'), JSON.stringify({
|
|
88
|
+
version: 1,
|
|
89
|
+
enabled: true,
|
|
90
|
+
disclosed: true,
|
|
91
|
+
installationId: '11111111-1111-4111-8111-111111111111',
|
|
92
|
+
token: 'source-token',
|
|
93
|
+
tokenExpiresAt: '2099-01-01T00:00:00.000Z',
|
|
94
|
+
}));
|
|
95
|
+
const network = relay();
|
|
96
|
+
let notice = '';
|
|
97
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: (value) => { notice += value; } });
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(await client.contributeJob(job), { shared: false, reason: 'grace' });
|
|
100
|
+
assert.match(notice, /confirmed public job links/i);
|
|
101
|
+
assert.equal(JSON.parse(await readFile(join(directory, 'source-sharing.json'), 'utf8')).jobSharingDisclosed, true);
|
|
102
|
+
assert.equal(network.requests.length, 0);
|
|
103
|
+
notice = '';
|
|
104
|
+
assert.equal((await client.contributeJob(job)).shared, true);
|
|
105
|
+
assert.equal(notice, '');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('an interrupted job-sharing disclosure still preserves the grace command', async (t) => {
|
|
109
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-job-grace-recovery-'));
|
|
110
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
111
|
+
await writeFile(join(directory, 'source-sharing.json'), JSON.stringify({
|
|
112
|
+
version: 1,
|
|
113
|
+
enabled: true,
|
|
114
|
+
disclosed: true,
|
|
115
|
+
jobSharingDisclosed: true,
|
|
116
|
+
jobSharingGraceConsumed: false,
|
|
117
|
+
installationId: null,
|
|
118
|
+
token: null,
|
|
119
|
+
tokenExpiresAt: null,
|
|
120
|
+
}));
|
|
121
|
+
const network = relay();
|
|
122
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: () => {} });
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(await client.contributeJob(job), { shared: false, reason: 'grace' });
|
|
125
|
+
assert.equal(network.requests.length, 0);
|
|
126
|
+
assert.equal((await client.contributeJob(job)).shared, true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('historical applications without a source config receive the disclosure grace command', async (t) => {
|
|
130
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-historical-no-config-'));
|
|
131
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
132
|
+
await writeFile(join(directory, 'applications.ndjson'), '{"historical":true}\n');
|
|
133
|
+
const network = relay();
|
|
134
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: () => {} });
|
|
135
|
+
|
|
136
|
+
assert.deepEqual(await client.contributeJob(job), { shared: false, reason: 'grace' });
|
|
137
|
+
assert.equal(network.requests.length, 0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('confirmed jobs are sanitized, shared anonymously, and public listings are validated', async (t) => {
|
|
141
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-job-'));
|
|
142
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
143
|
+
const network = relay();
|
|
144
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: () => {} });
|
|
145
|
+
|
|
146
|
+
const contributed = await client.contributeJob(job);
|
|
147
|
+
assert.deepEqual(contributed, { shared: true, jobId: 'community-job-abcdef1234567890', publicationStatus: 'pending', contributionCount: 2 });
|
|
148
|
+
const posted = network.requests.find((request) => request.url.endsWith('/v1/jobs') && request.options.method === 'POST');
|
|
149
|
+
assert.equal(posted.body.job.url, 'https://jobs.ashbyhq.com/example/12345678-1234-4123-8123-123456789abc');
|
|
150
|
+
assert.equal(posted.body.job.providerUrl, 'https://jobs.ashbyhq.com/example');
|
|
151
|
+
assert.equal(JSON.stringify(posted.body).includes('candidate@example.com'), false);
|
|
152
|
+
|
|
153
|
+
const listed = await client.listJobs({ limit: 25, cursor: 'current-page' });
|
|
154
|
+
assert.equal(listed.jobs[0].jobId, 'community-job-abcdef1234567890');
|
|
155
|
+
assert.equal(listed.jobs[0].contributionCount, 2);
|
|
156
|
+
assert.equal(listed.nextCursor, 'next-page');
|
|
157
|
+
assert.equal(network.requests.at(-1).url, 'https://relay.example.com/v1/jobs?limit=25&cursor=current-page');
|
|
158
|
+
const requestsBeforeInvalidInput = network.requests.length;
|
|
159
|
+
await assert.rejects(() => client.listJobs({ limit: 0 }), /between 1 and 100/i);
|
|
160
|
+
assert.equal(network.requests.length, requestsBeforeInvalidInput);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('community source listing validates the public response before reuse', async (t) => {
|
|
164
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-list-'));
|
|
165
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
166
|
+
const network = relay();
|
|
167
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: () => {} });
|
|
168
|
+
const [listed] = await client.list();
|
|
169
|
+
assert.equal(listed.sourceId, 'community-abcdef1234567890');
|
|
170
|
+
assert.equal(listed.baseUrl, 'https://jobs.example.org/openings/engineering');
|
|
171
|
+
assert.equal(listed.contributionCount, 2);
|
|
172
|
+
assert.equal(listed.registryStatus, 'community-reviewed');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('source sharing can be disabled independently and never blocks local collection', async (t) => {
|
|
176
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-disabled-'));
|
|
177
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
178
|
+
const network = relay();
|
|
179
|
+
let notice = '';
|
|
180
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: (value) => { notice += value; } });
|
|
181
|
+
|
|
182
|
+
assert.equal((await client.configure('disable')).enabled, false);
|
|
183
|
+
assert.deepEqual(await client.contribute(source), { shared: false, reason: 'disabled' });
|
|
184
|
+
assert.deepEqual(await client.contributeJob(job), { shared: false, reason: 'disabled' });
|
|
185
|
+
assert.equal(network.requests.length, 0);
|
|
186
|
+
assert.equal((await client.configure('enable')).enabled, true);
|
|
187
|
+
assert.match(notice, /confirmed public job links/i);
|
|
188
|
+
assert.equal((await client.contribute(source)).shared, true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('source sharing rejects personal and one-off job URLs before network transmission', async (t) => {
|
|
192
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-reject-'));
|
|
193
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
194
|
+
const network = relay();
|
|
195
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: network.fetch, stderr: () => {} });
|
|
196
|
+
|
|
197
|
+
await assert.rejects(() => client.preview({ ...source, baseUrl: 'https://linkedin.com/in/some-person' }), /profile or personal/i);
|
|
198
|
+
await assert.rejects(() => client.preview({ ...source, baseUrl: 'https://jobs.example.org/jobs/123456' }), /repeatable discovery surface/i);
|
|
199
|
+
await assert.rejects(() => client.preview({ ...source, baseUrl: 'https://jobs.example.org/candidate@example.com/openings' }), /identity-like content/i);
|
|
200
|
+
await assert.rejects(() => client.preview({ ...source, baseUrl: 'https://127.0.0.1/jobs' }), /public internet hostname/i);
|
|
201
|
+
await assert.rejects(() => client.preview({ ...source, baseUrl: 'https://careers.internal.local/jobs' }), /public internet hostname/i);
|
|
202
|
+
assert.equal(network.requests.length, 0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('network failures are best effort and return an unavailable result', async (t) => {
|
|
206
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-offline-'));
|
|
207
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
208
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: async () => { throw new Error('offline'); }, stderr: () => {} });
|
|
209
|
+
assert.deepEqual(await client.contribute(source), { shared: false, reason: 'unavailable' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('a failed contribution does not suppress a healthy community registry read', async (t) => {
|
|
213
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-independent-read-'));
|
|
214
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
215
|
+
const network = relay();
|
|
216
|
+
const fetch = async (url, options = {}) => {
|
|
217
|
+
if (url.endsWith('/v1/sources') && options.method === 'POST') return Response.json({ error: 'rate_limited' }, { status: 429 });
|
|
218
|
+
return network.fetch(url, options);
|
|
219
|
+
};
|
|
220
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
221
|
+
|
|
222
|
+
assert.deepEqual(await client.contribute(source), { shared: false, reason: 'unavailable' });
|
|
223
|
+
const [listed] = await client.list();
|
|
224
|
+
assert.equal(listed.sourceId, 'community-abcdef1234567890');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('concurrent opt-out is preserved and rechecked before source transmission', async (t) => {
|
|
228
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-opt-out-race-'));
|
|
229
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
230
|
+
await writeFile(join(directory, 'source-sharing.json'), JSON.stringify({
|
|
231
|
+
version: 1,
|
|
232
|
+
enabled: true,
|
|
233
|
+
disclosed: true,
|
|
234
|
+
installationId: null,
|
|
235
|
+
token: null,
|
|
236
|
+
tokenExpiresAt: null,
|
|
237
|
+
}));
|
|
238
|
+
let releaseInstall;
|
|
239
|
+
let installStarted;
|
|
240
|
+
const installGate = new Promise((resolve) => { releaseInstall = resolve; });
|
|
241
|
+
const installObserved = new Promise((resolve) => { installStarted = resolve; });
|
|
242
|
+
const sourcePosts = [];
|
|
243
|
+
const fetch = async (url, options = {}) => {
|
|
244
|
+
if (url.endsWith('/v1/install')) {
|
|
245
|
+
installStarted();
|
|
246
|
+
await installGate;
|
|
247
|
+
return Response.json({ installationId: '11111111-1111-4111-8111-111111111111', token: 'source-token', expiresAt: '2099-01-01T00:00:00.000Z' }, { status: 201 });
|
|
248
|
+
}
|
|
249
|
+
sourcePosts.push({ url, options });
|
|
250
|
+
return Response.json({ accepted: true, sourceId: 'community-abcdef1234567890', publicationStatus: 'pending', uniqueContributors: 1 }, { status: 202 });
|
|
251
|
+
};
|
|
252
|
+
const contributor = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
253
|
+
const settings = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
254
|
+
|
|
255
|
+
const contribution = contributor.contribute(source);
|
|
256
|
+
await installObserved;
|
|
257
|
+
assert.equal((await settings.configure('disable')).enabled, false);
|
|
258
|
+
releaseInstall();
|
|
259
|
+
|
|
260
|
+
assert.deepEqual(await contribution, { shared: false, reason: 'disabled' });
|
|
261
|
+
assert.equal(sourcePosts.length, 0);
|
|
262
|
+
assert.equal(JSON.parse(await readFile(join(directory, 'source-sharing.json'), 'utf8')).enabled, false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('concurrent reset is not undone by an in-flight credential refresh', async (t) => {
|
|
266
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-reset-race-'));
|
|
267
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
268
|
+
await writeFile(join(directory, 'source-sharing.json'), JSON.stringify({
|
|
269
|
+
version: 1,
|
|
270
|
+
enabled: true,
|
|
271
|
+
disclosed: true,
|
|
272
|
+
installationId: null,
|
|
273
|
+
token: null,
|
|
274
|
+
tokenExpiresAt: null,
|
|
275
|
+
}));
|
|
276
|
+
let releaseInstall;
|
|
277
|
+
let installStarted;
|
|
278
|
+
const installGate = new Promise((resolve) => { releaseInstall = resolve; });
|
|
279
|
+
const installObserved = new Promise((resolve) => { installStarted = resolve; });
|
|
280
|
+
const fetch = async (url) => {
|
|
281
|
+
if (!url.endsWith('/v1/install')) throw new Error('source contribution must remain disabled');
|
|
282
|
+
installStarted();
|
|
283
|
+
await installGate;
|
|
284
|
+
return Response.json({ installationId: '11111111-1111-4111-8111-111111111111', token: 'source-token', expiresAt: '2099-01-01T00:00:00.000Z' }, { status: 201 });
|
|
285
|
+
};
|
|
286
|
+
const contributor = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
287
|
+
const settings = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
288
|
+
|
|
289
|
+
const contribution = contributor.contribute(source);
|
|
290
|
+
await installObserved;
|
|
291
|
+
assert.deepEqual(await settings.configure('reset'), {
|
|
292
|
+
enabled: false,
|
|
293
|
+
disclosed: true,
|
|
294
|
+
jobSharingDisclosed: false,
|
|
295
|
+
hasInstallationId: false,
|
|
296
|
+
endpoint: 'https://relay.example.com',
|
|
297
|
+
schemaVersion: 1,
|
|
298
|
+
});
|
|
299
|
+
releaseInstall();
|
|
300
|
+
|
|
301
|
+
assert.deepEqual(await contribution, { shared: false, reason: 'disabled' });
|
|
302
|
+
const stored = JSON.parse(await readFile(join(directory, 'source-sharing.json'), 'utf8'));
|
|
303
|
+
assert.equal(stored.enabled, false);
|
|
304
|
+
assert.equal(stored.installationId, null);
|
|
305
|
+
assert.equal(stored.token, null);
|
|
306
|
+
assert.equal(stored.tokenExpiresAt, null);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('recovers a source-sharing lock whose owner process no longer exists', async (t) => {
|
|
310
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-stale-lock-'));
|
|
311
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
312
|
+
await writeFile(join(directory, '.source-sharing.lock'), '99999999\n');
|
|
313
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch: async () => { throw new Error('network must not be used'); }, stderr: () => {} });
|
|
314
|
+
|
|
315
|
+
assert.equal((await client.configure('disable')).enabled, false);
|
|
316
|
+
await assert.rejects(() => stat(join(directory, '.source-sharing.lock')), { code: 'ENOENT' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('invalid stored relay credentials are replaced once and persisted', async (t) => {
|
|
320
|
+
const directory = await mkdtemp(join(tmpdir(), 'source-community-credential-recovery-'));
|
|
321
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
322
|
+
const oldInstallationId = '11111111-1111-4111-8111-111111111111';
|
|
323
|
+
const newInstallationId = '22222222-2222-4222-8222-222222222222';
|
|
324
|
+
await writeFile(join(directory, 'source-sharing.json'), JSON.stringify({
|
|
325
|
+
version: 1,
|
|
326
|
+
enabled: true,
|
|
327
|
+
disclosed: true,
|
|
328
|
+
installationId: oldInstallationId,
|
|
329
|
+
token: 'invalid-old-token',
|
|
330
|
+
tokenExpiresAt: '2099-01-01T00:00:00.000Z',
|
|
331
|
+
}));
|
|
332
|
+
const requests = [];
|
|
333
|
+
const fetch = async (url, options = {}) => {
|
|
334
|
+
const body = options.body ? JSON.parse(options.body) : null;
|
|
335
|
+
requests.push({ url, body });
|
|
336
|
+
if (url.endsWith('/v1/sources') && body.installationId === oldInstallationId) return Response.json({ error: 'invalid_token' }, { status: 401 });
|
|
337
|
+
if (url.endsWith('/v1/install') && body.installationId === oldInstallationId) return Response.json({ error: 'invalid_token' }, { status: 401 });
|
|
338
|
+
if (url.endsWith('/v1/install')) return Response.json({ installationId: newInstallationId, token: 'new-token', expiresAt: '2099-01-01T00:00:00.000Z' }, { status: 201 });
|
|
339
|
+
return Response.json({ accepted: true, sourceId: 'community-abcdef1234567890', publicationStatus: 'pending', uniqueContributors: 1 }, { status: 202 });
|
|
340
|
+
};
|
|
341
|
+
const client = new SourceCommunityClient({ stateDir: directory, endpoint: 'https://relay.example.com', fetch, stderr: () => {} });
|
|
342
|
+
|
|
343
|
+
const result = await client.contribute(source);
|
|
344
|
+
|
|
345
|
+
assert.equal(result.shared, true);
|
|
346
|
+
assert.ok(requests.some((request) => request.url.endsWith('/v1/install') && request.body.installationId === oldInstallationId));
|
|
347
|
+
assert.ok(requests.some((request) => request.url.endsWith('/v1/install') && Object.keys(request.body).length === 0));
|
|
348
|
+
const stored = JSON.parse(await readFile(join(directory, 'source-sharing.json'), 'utf8'));
|
|
349
|
+
assert.equal(stored.installationId, newInstallationId);
|
|
350
|
+
assert.equal(stored.token, 'new-token');
|
|
351
|
+
});
|