job-application-agent 3.2.0 → 3.2.1
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/job-application-agent/scripts/source-community-schema.mjs +6 -0
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/job-application.test.mjs +14 -6
- package/job-application-agent/tests/source-community-schema.test.mjs +2 -1
- package/job-application-agent/tests/workflow-state.test.mjs +1 -1
- package/package.json +1 -1
|
@@ -74,6 +74,11 @@ function isPublicHostname(hostname) {
|
|
|
74
74
|
return value.includes('.');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function isReservedExampleHostname(hostname) {
|
|
78
|
+
const value = hostname.toLowerCase();
|
|
79
|
+
return ['example.com', 'example.net', 'example.org'].some((suffix) => value === suffix || value.endsWith(`.${suffix}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
function hostnameMatches(hostname, suffix) {
|
|
78
83
|
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
79
84
|
}
|
|
@@ -139,6 +144,7 @@ export function normalizeCommunityJob(input) {
|
|
|
139
144
|
if (url.protocol !== 'https:' || url.username || url.password) throw new Error('community job.url must be a public HTTPS URL.');
|
|
140
145
|
url.hostname = url.hostname.replace(/\.+$/, '').toLowerCase();
|
|
141
146
|
if (!isPublicHostname(url.hostname)) throw new Error('community job.url must use a public HTTPS hostname.');
|
|
147
|
+
if (isReservedExampleHostname(url.hostname)) throw new Error('community job.url must not use a reserved example hostname.');
|
|
142
148
|
const pathname = decodedPathname(url.pathname).normalize('NFKC');
|
|
143
149
|
url.pathname = pathname;
|
|
144
150
|
if (containsEmailLike(pathname) || containsPhoneLikeLocation(url.hostname, pathname) || looksIdentityPath(pathname) || looksPersonal(url)) throw new Error('community job.url must not be a personal URL.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SKILL_VERSION = '3.2.
|
|
1
|
+
export const SKILL_VERSION = '3.2.1';
|
|
@@ -48,6 +48,14 @@ const matchingJob = {
|
|
|
48
48
|
],
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
function isolatedCliEnv(directory) {
|
|
52
|
+
return {
|
|
53
|
+
...process.env,
|
|
54
|
+
JOB_APPLICATION_AGENT_STATE_DIR: directory,
|
|
55
|
+
JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function runCli(script, args, input, env) {
|
|
52
60
|
return new Promise((resolve) => {
|
|
53
61
|
const child = spawn(process.execPath, [script, ...args], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -73,7 +81,7 @@ test('returns the canonical resume path for direct browser uploads', async (t) =
|
|
|
73
81
|
const resume = join(directory, 'resume.pdf');
|
|
74
82
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
75
83
|
await writeFile(resume, '%PDF-1.7\ncanonical resume fixture');
|
|
76
|
-
const env =
|
|
84
|
+
const env = isolatedCliEnv(directory);
|
|
77
85
|
|
|
78
86
|
const result = JSON.parse(execFileSync(process.execPath, [script, 'resume', 'path'], { env, encoding: 'utf8' }));
|
|
79
87
|
|
|
@@ -246,7 +254,7 @@ test('deduplicates ledger entries by normalized URL', async (t) => {
|
|
|
246
254
|
telemetry: { durationBucket: '5-15m', fieldsFilled: 14, shortAnswerCount: 2, resumeUploaded: true },
|
|
247
255
|
};
|
|
248
256
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
249
|
-
const env =
|
|
257
|
+
const env = isolatedCliEnv(directory);
|
|
250
258
|
execFileSync(process.execPath, [script, 'ledger', 'add', '--stdin'], { input: JSON.stringify(entry), env, encoding: 'utf8' });
|
|
251
259
|
const duplicate = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], { input: JSON.stringify({ id: 'different', url: 'https://jobs.example.com/123?ref=friend' }), env, encoding: 'utf8' }));
|
|
252
260
|
assert.equal(duplicate.duplicate, true);
|
|
@@ -263,7 +271,7 @@ test('warns on same-company role matches and serializes concurrent duplicate sub
|
|
|
263
271
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
264
272
|
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
265
273
|
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
266
|
-
const env =
|
|
274
|
+
const env = isolatedCliEnv(directory);
|
|
267
275
|
const base = {
|
|
268
276
|
company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
|
|
269
277
|
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
|
|
@@ -291,7 +299,7 @@ test('records structured outcomes idempotently without duplicate rows', async (t
|
|
|
291
299
|
id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
|
|
292
300
|
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
293
301
|
})}\n`);
|
|
294
|
-
const env =
|
|
302
|
+
const env = isolatedCliEnv(directory);
|
|
295
303
|
const outcome = { id: 'example-role-1', status: 'rejected', occurredAt: '2026-01-20T09:00:00Z', note: 'No sponsorship' };
|
|
296
304
|
const enriched = { ...outcome, reasons: [{ category: 'eligibility', evidence: 'explicit' }] };
|
|
297
305
|
const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(outcome), env, encoding: 'utf8' }));
|
|
@@ -314,7 +322,7 @@ test('records bounded interview quality and failure-point enrichment idempotentl
|
|
|
314
322
|
id: 'example-role-1', company: 'Example', role: 'Staff Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 91,
|
|
315
323
|
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
316
324
|
})}\n`);
|
|
317
|
-
const env =
|
|
325
|
+
const env = isolatedCliEnv(directory);
|
|
318
326
|
const base = { id: 'example-role-1', status: 'interview', occurredAt: '2026-01-20T09:00:00Z' };
|
|
319
327
|
const enriched = { ...base, interviewQuality: 'weak', failurePoint: 'role-scope' };
|
|
320
328
|
const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(base), env, encoding: 'utf8' }));
|
|
@@ -400,7 +408,7 @@ test('acknowledges a generated review only through the explicit CLI command', as
|
|
|
400
408
|
source: 'company', score: 80, status: 'submitted', submittedAt: '2026-01-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
401
409
|
}));
|
|
402
410
|
await writeFile(join(directory, 'applications.ndjson'), `${entries.map(JSON.stringify).join('\n')}\n`);
|
|
403
|
-
const env =
|
|
411
|
+
const env = isolatedCliEnv(directory);
|
|
404
412
|
const before = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review'], { env, encoding: 'utf8' }));
|
|
405
413
|
assert.equal(before.reviewDue, true);
|
|
406
414
|
assert.equal(await readFile(join(directory, 'reviews.ndjson'), 'utf8').catch(() => ''), '');
|
|
@@ -53,6 +53,7 @@ test('accepts public job detail identifiers but rejects private, personal, and c
|
|
|
53
53
|
|
|
54
54
|
const rejected = [
|
|
55
55
|
'http://localhost/jobs/123',
|
|
56
|
+
'https://jobs.example.com/123',
|
|
56
57
|
'https://linkedin.com/in/some-person',
|
|
57
58
|
'https://company.example/candidate/9876543210',
|
|
58
59
|
'https://company.example/referral/12345678-1234-4123-8123-123456789abc',
|
|
@@ -61,7 +62,7 @@ test('accepts public job detail identifiers but rejects private, personal, and c
|
|
|
61
62
|
'https://company.example/jobs/access-token=abcdefghijklmnop',
|
|
62
63
|
'https://user:password@company.example/jobs/123',
|
|
63
64
|
];
|
|
64
|
-
for (const url of rejected) assert.throws(() => normalizeCommunityJob({ ...job, url }), /public HTTPS|personal|credential/i, url);
|
|
65
|
+
for (const url of rejected) assert.throws(() => normalizeCommunityJob({ ...job, url }), /public HTTPS|personal|credential|reserved example/i, url);
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
test('preserves only stable query job identifiers while removing referral data', async () => {
|
|
@@ -56,7 +56,7 @@ function submission(index, roundId, overrides = {}) {
|
|
|
56
56
|
id: `round-role-${index}`,
|
|
57
57
|
company: `Company ${index}`,
|
|
58
58
|
role: 'Senior Product Engineer',
|
|
59
|
-
url: `https://jobs.example
|
|
59
|
+
url: `https://jobs.fixture.example/${index}`,
|
|
60
60
|
employerJobId: `example:${index}`,
|
|
61
61
|
source: 'ashby',
|
|
62
62
|
discoverySource: 'linkedin',
|
package/package.json
CHANGED