job-application-agent 3.4.2 → 3.6.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 +23 -0
- package/installer/src/cli.mjs +8 -1
- package/installer/src/installer.mjs +8 -0
- package/job-application-agent/SKILL.md +32 -2
- package/job-application-agent/capabilities.json +3 -1
- package/job-application-agent/references/ACCOUNTING.md +104 -0
- package/job-application-agent/references/AUTONOMY.md +2 -0
- package/job-application-agent/references/CLOUD_STATE.md +2 -0
- package/job-application-agent/references/FREE_AI.md +39 -0
- package/job-application-agent/references/OUTREACH.md +210 -0
- package/job-application-agent/references/RUNS.md +35 -0
- package/job-application-agent/references/SCHEMAS.md +12 -2
- package/job-application-agent/references/agent-box/README.md +77 -0
- package/job-application-agent/references/agent-box/novnc.service.example +16 -0
- package/job-application-agent/scripts/application-accounting.mjs +246 -0
- package/job-application-agent/scripts/ats/answer-inject.mjs +203 -0
- package/job-application-agent/scripts/ats/submit-adapters.mjs +194 -0
- package/job-application-agent/scripts/attention-questions.mjs +111 -0
- package/job-application-agent/scripts/attention-resume-submit.mjs +450 -0
- package/job-application-agent/scripts/attention-runner-poll.mjs +328 -0
- package/job-application-agent/scripts/captcha-vendor.mjs +328 -0
- package/job-application-agent/scripts/cloud-state-client.mjs +62 -8
- package/job-application-agent/scripts/job-application.mjs +243 -27
- package/job-application-agent/scripts/novnc-display-guard.mjs +300 -0
- package/job-application-agent/scripts/outreach-cli.mjs +95 -0
- package/job-application-agent/scripts/outreach-domain.mjs +287 -0
- package/job-application-agent/scripts/outreach-store.mjs +72 -0
- package/job-application-agent/scripts/session-binding.mjs +474 -0
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/accounting-cli.test.mjs +86 -0
- package/job-application-agent/tests/accounting-cloud-client.test.mjs +183 -0
- package/job-application-agent/tests/accounting-retry-cli.test.mjs +96 -0
- package/job-application-agent/tests/accounting-source-race.test.mjs +109 -0
- package/job-application-agent/tests/answer-inject-captcha.test.mjs +169 -0
- package/job-application-agent/tests/application-accounting.test.mjs +315 -0
- package/job-application-agent/tests/attention-resume-submit.test.mjs +119 -0
- package/job-application-agent/tests/attention-runner-poll.test.mjs +135 -0
- package/job-application-agent/tests/fixtures/outreach.mjs +22 -0
- package/job-application-agent/tests/job-application.test.mjs +5 -0
- package/job-application-agent/tests/novnc-display-guard.test.mjs +50 -0
- package/job-application-agent/tests/outreach-cli.test.mjs +73 -0
- package/job-application-agent/tests/outreach.test.mjs +178 -0
- package/job-application-agent/tests/privacy-audit.test.mjs +2 -0
- package/job-application-agent/tests/review-cadence.test.mjs +66 -0
- package/job-application-agent/tests/session-binding.test.mjs +102 -0
- package/job-application-agent/tests/skill-contract.test.mjs +25 -0
- package/job-application-agent/tests/workflow-state.test.mjs +30 -3
- package/package.json +6 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { outreachCache, runOutreach } from '../scripts/outreach-cli.mjs';
|
|
10
|
+
import { migrateLegacyStateDir, legacyMacStateDir, resolveStateDir } from '../scripts/secret-store.mjs';
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const cli = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
13
|
+
|
|
14
|
+
test('CLI outreach success and errors bypass telemetry, identity, and community network paths', async () => {
|
|
15
|
+
const dir = await mkdtemp(join(tmpdir(), 'outreach-cli-'));
|
|
16
|
+
try {
|
|
17
|
+
const trap = join(dir, 'network-trap.mjs');
|
|
18
|
+
await writeFile(trap, "import { appendFileSync } from 'node:fs'; globalThis.fetch = async () => { appendFileSync(process.env.NETWORK_MARKER, 'called'); throw new Error('NETWORK_TRAP'); };\n");
|
|
19
|
+
const run = args => exec(process.execPath, ['--import', pathToFileURL(trap).href, cli, 'outreach', ...args], { env: { ...process.env, NETWORK_MARKER: join(dir, 'network-called'), JOB_APPLICATION_AGENT_STATE_DIR: dir, JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json') } });
|
|
20
|
+
const result = await run(['policy', 'status']);
|
|
21
|
+
assert.equal(JSON.parse(result.stdout).enabled, false);
|
|
22
|
+
await assert.rejects(run(['invented']), error => !error.stderr.includes('NETWORK_TRAP') && /outreach/i.test(error.stderr));
|
|
23
|
+
for (const name of ['telemetry.json', 'source-sharing.json', 'telemetry-identity.json']) await assert.rejects(readFile(join(dir, name)), { code: 'ENOENT' });
|
|
24
|
+
await assert.rejects(readFile(join(dir, 'network-called')), { code: 'ENOENT' });
|
|
25
|
+
} finally { await rm(dir, { recursive: true, force: true }); }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('late snapshots cannot undo mutation invalidation or a newer clear snapshot', async () => {
|
|
29
|
+
const dir = await mkdtemp(join(tmpdir(), 'outreach-cache-race-'));
|
|
30
|
+
try {
|
|
31
|
+
const { generation } = await outreachCache(dir, 'synthetic-binding', 'read');
|
|
32
|
+
await outreachCache(dir, 'synthetic-binding', 'invalidate');
|
|
33
|
+
const rejected = await outreachCache(dir, 'synthetic-binding', 'write', { generation, snapshot: { revision: 1, privateText: 'old' } });
|
|
34
|
+
assert.equal(rejected.snapshot, undefined);
|
|
35
|
+
await outreachCache(dir, 'synthetic-binding', 'write', { generation: rejected.generation, snapshot: { revision: 3, cleared: true } });
|
|
36
|
+
const current = await outreachCache(dir, 'synthetic-binding', 'write', { generation: rejected.generation, snapshot: { revision: 2, privateText: 'old' } });
|
|
37
|
+
assert.equal(current.snapshot, undefined);
|
|
38
|
+
assert.equal(current.generation, rejected.generation + 1);
|
|
39
|
+
assert.equal(JSON.stringify(current).includes('old'), false);
|
|
40
|
+
} finally { await rm(dir, { recursive: true, force: true }); }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
test('outreach first-run migrates legacy state before creating its database', async () => {
|
|
45
|
+
const home = await mkdtemp(join(tmpdir(), 'outreach-legacy-'));
|
|
46
|
+
try {
|
|
47
|
+
const options = { home, env: {}, plat: 'darwin' };
|
|
48
|
+
const legacy = legacyMacStateDir(home), destination = resolveStateDir(options);
|
|
49
|
+
await mkdir(legacy, { recursive: true });
|
|
50
|
+
await writeFile(join(legacy, 'applications.ndjson'), '{"id":"legacy"}\n');
|
|
51
|
+
await runOutreach(['policy', 'status'], { stateDirectory: destination,
|
|
52
|
+
cloudClient: { config: async () => null }, migrate: directory => migrateLegacyStateDir(directory, options) });
|
|
53
|
+
assert.equal(await readFile(join(destination, 'applications.ndjson'), 'utf8'), '{"id":"legacy"}\n');
|
|
54
|
+
} finally { await rm(home, { recursive: true, force: true }); }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a lower authoritative revision removes pre-restore sensitive cache before offline use', async () => {
|
|
58
|
+
const dir = await mkdtemp(join(tmpdir(), 'outreach-restored-cache-'));
|
|
59
|
+
try {
|
|
60
|
+
const binding = 'same-worker-and-token';
|
|
61
|
+
const { generation } = await outreachCache(dir, binding, 'read');
|
|
62
|
+
await outreachCache(dir, binding, 'write', { generation, snapshot: { revision: 50, privateText: 'pre-restore' } });
|
|
63
|
+
await outreachCache(dir, binding, 'write', { generation, snapshot: { revision: 3, items: [], policy: { recoveryBlocked: true } } });
|
|
64
|
+
const cache = await outreachCache(dir, binding, 'read');
|
|
65
|
+
assert.equal(cache.snapshot, undefined);
|
|
66
|
+
assert.equal((await readFile(join(dir, 'outreach-cloud-cache.json'), 'utf8')).includes('pre-restore'), false);
|
|
67
|
+
// A delayed pre-restore response cannot refill the invalidated cache.
|
|
68
|
+
await outreachCache(dir, binding, 'write', { generation, snapshot: { revision: 51, privateText: 'pre-restore' } });
|
|
69
|
+
assert.equal((await outreachCache(dir, binding, 'read')).snapshot, undefined);
|
|
70
|
+
await outreachCache(dir, binding, 'write', { generation: cache.generation, snapshot: { revision: 3, items: [] } });
|
|
71
|
+
assert.deepEqual((await outreachCache(dir, binding, 'read')).snapshot.items, []);
|
|
72
|
+
} finally { await rm(dir, { recursive: true, force: true }); }
|
|
73
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { initialOutreach, mutateOutreach, readOutreach, businessDate } from '../scripts/outreach-domain.mjs';
|
|
4
|
+
|
|
5
|
+
import { assessment, fixture, handoff, now } from './fixtures/outreach.mjs';
|
|
6
|
+
|
|
7
|
+
test('qualified hiring posts can be drafted and handed off without an application or transmission', () => {
|
|
8
|
+
const f = fixture();
|
|
9
|
+
const result = f.run('handoff', handoff());
|
|
10
|
+
assert.equal(result.delivery, 'pending-handoff');
|
|
11
|
+
assert.match(result.copyableText, /product engineering/);
|
|
12
|
+
assert.equal(readOutreach(f.state, 'review', {}, now).sentVerified, 0);
|
|
13
|
+
assert.deepEqual(f.run('handoff', handoff()), result);
|
|
14
|
+
assert.throws(() => f.run('handoff', handoff({ draftRevision: 2 })), /operation.*content/i);
|
|
15
|
+
});
|
|
16
|
+
test('false qualification and missing candidate claim references block handoff or draft', () => {
|
|
17
|
+
const f = fixture();
|
|
18
|
+
assert.throws(() => f.run('draft', { operationId: 'bad', id: 'opportunity-1', text: 'Hello', claimRefs: ['invented'], purpose: 'initial' }), /claim/i);
|
|
19
|
+
f.run('assess', assessment('opportunity-1', { operationId: 'reassess', qualification: { active: true, companyVerified: true, eligible: false, fit: true, affiliation: true, hiringInvolvement: true } }));
|
|
20
|
+
assert.throws(() => f.run('handoff', handoff({ qualificationRevision: 2 })), /qualification/i);
|
|
21
|
+
});
|
|
22
|
+
test('company reservation blocks other channels and does not expire after uncertain handoff', () => {
|
|
23
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
24
|
+
f.run('record', { operationId: 'uncertain', id: 'opportunity-1', type: 'uncertain', attemptId: 'handoff-1', occurredAt: now, evidence: 'User cannot confirm sending.' });
|
|
25
|
+
f.run('assess', assessment('second', { channel: 'x', recipient: { account: 'https://x.com/example_hiring', aliases: [] } }));
|
|
26
|
+
f.run('draft', { operationId: 'draft-2', id: 'second', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
27
|
+
assert.throws(() => f.run('handoff', handoff({ operationId: 'handoff-2', id: 'second' })), /reserved|contact/i);
|
|
28
|
+
});
|
|
29
|
+
test('clear removes sensitive material, retains suppression, and refuses resurrection', () => {
|
|
30
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
31
|
+
f.run('clear', { operationId: 'clear', ids: ['opportunity-1'] });
|
|
32
|
+
const serialized = JSON.stringify(f.state);
|
|
33
|
+
for (const privateText of ['example.org', 'example-recruiter', 'canonical-resume', 'brief conversation']) assert.equal(serialized.includes(privateText), false);
|
|
34
|
+
assert.throws(() => f.run('assess', assessment('opportunity-1', { operationId: 'resurrect' })), /cleared/i);
|
|
35
|
+
assert.equal(f.run('handoff', handoff()).cleared, true);
|
|
36
|
+
});
|
|
37
|
+
test('business dates use local calendar weekdays across weekends', () => {
|
|
38
|
+
assert.equal(businessDate('2026-09-18T23:30:00Z', 'Asia/Kolkata', 7), '2026-09-29');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('ranking does not exclude qualified contacts and aliases identify the same recipient', () => {
|
|
42
|
+
const f = fixture();
|
|
43
|
+
f.run('assess', assessment('opportunity-1', { operationId: 'low-rank', ranking: { hiringSignal: 0, responsibility: 0, fit: 0, freshness: 0, relationship: 0 } }));
|
|
44
|
+
f.run('draft', { operationId: 'new-draft', id: 'opportunity-1', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
45
|
+
assert.equal(f.run('handoff', handoff({ draftRevision: 2, qualificationRevision: 2 })).score, 0);
|
|
46
|
+
f.run('assess', assessment('other-company', { company: { name: 'Second', domain: 'second.example', aliases: [] }, recipient: { account: 'https://linkedin.com/in/EXAMPLE-RECRUITER/?tracking=1', aliases: [] } }));
|
|
47
|
+
f.run('draft', { operationId: 'other-draft', id: 'other-company', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
48
|
+
assert.throws(() => f.run('handoff', handoff({ operationId: 'other-handoff', id: 'other-company' })), /reserved/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('follow-up depends on actual send dates, and replies cancel it', () => {
|
|
52
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
53
|
+
f.run('record', { operationId: 'sent', id: 'opportunity-1', attemptId: 'handoff-1', type: 'sent-user-reported', occurredAt: now, evidence: 'User reports manual send.' });
|
|
54
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, '2026-09-24T10:00:00Z').followupDue, false);
|
|
55
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, '2026-09-25T10:00:00Z').followupDue, true);
|
|
56
|
+
f.run('draft', { operationId: 'followup-draft', id: 'opportunity-1', text: 'Following up on the role.', claimRefs: [], purpose: 'follow-up' });
|
|
57
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, '2026-10-20T10:00:00Z').noResponse, false);
|
|
58
|
+
f.run('record', { operationId: 'reply', id: 'opportunity-1', type: 'screen-proposed', occurredAt: now, evidence: 'Recruiter proposed a discussion.' });
|
|
59
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, '2026-10-20T10:00:00Z').followupDue, false);
|
|
60
|
+
assert.equal(readOutreach(f.state, 'review', {}, now).outcomes['screen-scheduled'], 0);
|
|
61
|
+
assert.throws(() => f.run('record', { operationId: 'bad-schedule', id: 'opportunity-1', type: 'screen-scheduled', occurredAt: now, evidence: 'They mentioned a discussion.' }), /object/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('conflicting delivery evidence requires explicit correction; not-sent release can be corrected safely', () => {
|
|
65
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
66
|
+
const record = (operationId, type, extra = {}) => f.run('record', { operationId, id: 'opportunity-1', attemptId: 'handoff-1', type, occurredAt: now, evidence: 'Synthetic observation.', ...extra });
|
|
67
|
+
record('uncertain', 'uncertain');
|
|
68
|
+
assert.equal(record('sent', 'sent-user-reported').delivery, 'conflict');
|
|
69
|
+
assert.equal(record('fixed', 'not-sent', { supersedes: ['uncertain', 'sent'] }).delivery, 'not-sent');
|
|
70
|
+
assert.equal(Object.keys(f.state.reservations).length, 0);
|
|
71
|
+
assert.equal(f.run('handoff', handoff()).copyableText, undefined);
|
|
72
|
+
record('later-proof', 'sent-verified', { supersedes: ['fixed'], messageRef: 'message-1' });
|
|
73
|
+
assert.equal(Object.keys(f.state.reservations).length, 1);
|
|
74
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, now).delivery, 'sent-verified');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('linked applications must match both company and role', () => {
|
|
78
|
+
const f = fixture();
|
|
79
|
+
const input = assessment('applied', { applicationId: 'app-1', source: { kind: 'application', url: 'https://example.org/jobs/1' } });
|
|
80
|
+
assert.throws(() => mutateOutreach(f.state, 'assess', input, { now, applications: [{ id: 'app-1', company: 'Wrong Company', role: input.role, status: 'submitted' }] }), /company/i);
|
|
81
|
+
const result = mutateOutreach(f.state, 'assess', input, { now, applications: [{ id: 'app-1', company: 'Example', role: input.role, status: 'submitted' }] });
|
|
82
|
+
assert.equal(result.result.eligible, true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('common fabricated application and commitment claims are rejected; disabled grants stop handoff', () => {
|
|
86
|
+
const f = fixture();
|
|
87
|
+
for (const [index, text] of ['I applied for this role.', 'I am available immediately.', 'I built AI agents.'].entries()) {
|
|
88
|
+
assert.throws(() => f.run('draft', { operationId: `invalid-${index}`, id: 'opportunity-1', text, claimRefs: [], purpose: 'initial' }), /claim|commitment|evidence/);
|
|
89
|
+
}
|
|
90
|
+
f.run('policy-disable', { operationId: 'disable' });
|
|
91
|
+
assert.throws(() => f.run('handoff', handoff()), /disabled/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('recipient and company suppression cannot be overridden by an exception', () => {
|
|
95
|
+
const f = fixture();
|
|
96
|
+
f.run('suppress', { operationId: 'stop', id: 'opportunity-1', scope: 'company', reason: 'Candidate requested no contact.' });
|
|
97
|
+
assert.throws(() => f.run('handoff', handoff({ exception: { approvedByUser: true, reason: 'Attempted bypass.' } })), /suppressed/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('company suppression cancels due suggestions for other opportunities', () => {
|
|
101
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
102
|
+
f.run('record', { operationId: 'sent', id: 'opportunity-1', attemptId: 'handoff-1', type: 'sent-user-reported', occurredAt: now, evidence: 'User reports sending.' });
|
|
103
|
+
f.run('assess', assessment('other'));
|
|
104
|
+
f.run('suppress', { operationId: 'stop-other', id: 'other', scope: 'company', reason: 'Company-wide stop.' });
|
|
105
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, '2026-10-01T10:00:00Z').followupDue, false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('one follow-up can close as no response only after it was sent', () => {
|
|
109
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
110
|
+
f.run('record', { operationId: 'initial-sent', id: 'opportunity-1', type: 'sent-user-reported', attemptId: 'handoff-1', occurredAt: now, evidence: 'User reports sending.' });
|
|
111
|
+
f.run('draft', { operationId: 'followup', id: 'opportunity-1', text: 'Following up on the role.', claimRefs: [], purpose: 'follow-up' });
|
|
112
|
+
const later = '2026-09-25T10:00:00Z';
|
|
113
|
+
let state = mutateOutreach(f.state, 'handoff', handoff({ operationId: 'followup-handoff', draftRevision: 2, recheckedAt: later, history: { kind: 'verified', noPriorPitch: false, checkedAt: later } }), { now: later }).state;
|
|
114
|
+
assert.equal(readOutreach(state, 'show', { id: 'opportunity-1' }, '2026-10-20T10:00:00Z').noResponse, false);
|
|
115
|
+
state = mutateOutreach(state, 'record', { operationId: 'followup-sent', id: 'opportunity-1', type: 'sent-user-reported', attemptId: 'followup-handoff', occurredAt: later, evidence: 'User reports follow-up.' }, { now: later }).state;
|
|
116
|
+
assert.equal(readOutreach(state, 'show', { id: 'opportunity-1' }, '2026-10-06T10:00:00Z').noResponse, true);
|
|
117
|
+
assert.equal(readOutreach(state, 'show', { id: 'opportunity-1' }, '2026-10-06T10:00:00Z').followupDue, false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('progression corrections cannot supersede delivery evidence and bypass an unresolved reservation', () => {
|
|
121
|
+
const f = fixture(); f.run('handoff', handoff());
|
|
122
|
+
f.run('record', { operationId: 'sent', id: 'opportunity-1', type: 'sent-user-reported', attemptId: 'handoff-1', occurredAt: now, evidence: 'User reports sending.' });
|
|
123
|
+
assert.throws(() => f.run('record', { operationId: 'bad-correction', id: 'opportunity-1', type: 'replied', supersedes: ['sent'], occurredAt: now, evidence: 'Wrong category.' }), /category|delivery/i);
|
|
124
|
+
f.run('record', { operationId: 'uncertain-correction', id: 'opportunity-1', type: 'uncertain', attemptId: 'handoff-1', supersedes: ['sent'], occurredAt: now, evidence: 'Send could not be verified.' });
|
|
125
|
+
f.run('assess', assessment('second'));
|
|
126
|
+
f.run('draft', { operationId: 'second-draft', id: 'second', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
127
|
+
assert.throws(() => f.run('handoff', handoff({ operationId: 'second-handoff', id: 'second', exception: { approvedByUser: true, reason: 'Try another contact.' } })), /unresolved/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('valid opaque IDs may match Object.prototype property names', () => {
|
|
131
|
+
const f = fixture();
|
|
132
|
+
assert.throws(() => f.run('clear', { operationId: 'bad-clear', ids: ['constructor'] }), /not found/);
|
|
133
|
+
assert.equal(Object.hasOwn(f.state.reservations, 'clear-constructor'), false);
|
|
134
|
+
f.run('assess', assessment('constructor', { operationId: 'toString' }));
|
|
135
|
+
f.run('draft', { operationId: 'constructor', id: 'constructor', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
136
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'constructor' }, now).content.drafts.length, 1);
|
|
137
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'constructor' }, now).cleared, false);
|
|
138
|
+
assert.equal(f.run('draft', { operationId: 'constructor', id: 'constructor', text: 'Hello', claimRefs: [], purpose: 'initial' }).cleared, false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('clearing another opportunity cannot overwrite a pending handoff reservation', () => {
|
|
142
|
+
const f = fixture();
|
|
143
|
+
f.run('handoff', handoff({ operationId: 'clear-other' }));
|
|
144
|
+
f.run('assess', assessment('other', { company: { name: 'Other', domain: 'other.org', aliases: [] }, recipient: { account: 'https://x.com/other', aliases: [] }, channel: 'x' }));
|
|
145
|
+
f.run('clear', { operationId: 'clear-op', ids: ['other'] });
|
|
146
|
+
assert.equal(f.state.reservations['clear-other'].pending, true);
|
|
147
|
+
f.run('assess', assessment('retry'));
|
|
148
|
+
f.run('draft', { operationId: 'retry-draft', id: 'retry', text: 'Hello', claimRefs: [], purpose: 'initial' });
|
|
149
|
+
assert.throws(() => f.run('handoff', handoff({ operationId: 'retry-handoff', id: 'retry' })), /unresolved/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('correcting rejection removes only its derived suppression', () => {
|
|
153
|
+
const f = fixture();
|
|
154
|
+
f.run('record', { operationId: 'rejection', id: 'opportunity-1', type: 'rejected', occurredAt: now, evidence: 'Mistaken rejection.' });
|
|
155
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, now).suppressed, true);
|
|
156
|
+
f.run('record', { operationId: 'correction', id: 'opportunity-1', type: 'replied', supersedes: ['rejection'], occurredAt: now, evidence: 'Actually a reply.' });
|
|
157
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, now).suppressed, false);
|
|
158
|
+
f.run('suppress', { operationId: 'explicit', id: 'opportunity-1', scope: 'recipient', reason: 'User requests stop.' });
|
|
159
|
+
f.run('record', { operationId: 'correction-2', id: 'opportunity-1', type: 'referred', supersedes: ['correction'], occurredAt: now, evidence: 'Referral completed.' });
|
|
160
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'opportunity-1' }, now).suppressed, true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('company suppression does not suppress a recipient at an unrelated company', () => {
|
|
164
|
+
const f = fixture();
|
|
165
|
+
f.run('suppress', { operationId: 'stop-company', id: 'opportunity-1', scope: 'company', reason: 'Stop this company.' });
|
|
166
|
+
f.run('assess', assessment('new-company', { company: { name: 'Other', domain: 'other.org', aliases: [] } }));
|
|
167
|
+
assert.equal(readOutreach(f.state, 'show', { id: 'new-company' }, now).suppressed, false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('handoff retries recheck current linked application outcomes', () => {
|
|
171
|
+
const f = fixture();
|
|
172
|
+
const context = { now, applications: [{ id: 'app-1', company: 'Example', role: 'Staff Product Engineer', status: 'submitted' }] };
|
|
173
|
+
let state = mutateOutreach(f.state, 'assess', assessment('opportunity-1', { operationId: 'linked', applicationId: 'app-1' }), context).state;
|
|
174
|
+
state = mutateOutreach(state, 'draft', { operationId: 'linked-draft', id: 'opportunity-1', text: 'Hello', claimRefs: [], purpose: 'initial' }, context).state;
|
|
175
|
+
const input = handoff({ qualificationRevision: 2, draftRevision: 2 });
|
|
176
|
+
state = mutateOutreach(state, 'handoff', input, context).state;
|
|
177
|
+
assert.throws(() => mutateOutreach(state, 'handoff', input, { ...context, outcomes: [{ id: 'app-1', status: 'rejected' }] }), /hiring outcome/);
|
|
178
|
+
});
|
|
@@ -11,6 +11,8 @@ const forbiddenProperties = [
|
|
|
11
11
|
'agentResponse', 'jobDescription', 'formQuestion', 'draftedAnswer', 'note', 'password', 'mfa',
|
|
12
12
|
'captcha', 'legalAnswer', 'demographicAnswer', 'browserData', 'ipAddress', 'requestHeaders',
|
|
13
13
|
'userAgent', 'rawError',
|
|
14
|
+
'recipient', 'recipientAccount', 'message', 'sentText', 'claimRefs', 'evidence',
|
|
15
|
+
'outreachId', 'opportunityId', 'companyFingerprint', 'recipientFingerprint',
|
|
14
16
|
];
|
|
15
17
|
|
|
16
18
|
test('privacy audit rejects every prohibited free-form or identity property', () => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, 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 { buildReview } from '../scripts/job-application.mjs';
|
|
10
|
+
|
|
11
|
+
const now = new Date('2026-09-14T12:00:00Z');
|
|
12
|
+
const applications = (count, submittedAt = '2026-09-14T10:00:00Z') => Array.from({ length: count }, (_, index) => ({
|
|
13
|
+
id: `application-${index}`, company: `Company ${index}`, role: 'Engineer',
|
|
14
|
+
url: `https://example.test/jobs/${index}`, source: 'email', status: 'submitted', submittedAt,
|
|
15
|
+
}));
|
|
16
|
+
const failure = {
|
|
17
|
+
version: 1, id: 'late-bounce', applicationId: 'application-0', attemptId: 'initial:application-0',
|
|
18
|
+
type: 'delivery-failed', occurredAt: '2026-09-14T11:00:00Z',
|
|
19
|
+
evidenceType: 'final-delivery-failure', evidence: 'Final failure matched to the original application email.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('late failure does not delay hygiene review after ten more canonical submissions', () => {
|
|
23
|
+
const entries = applications(20);
|
|
24
|
+
const acknowledgements = [{ uniqueSubmissionCount: 10, maturedApplicationCount: 0 }];
|
|
25
|
+
const review = buildReview(entries, [], acknowledgements, now, [failure]);
|
|
26
|
+
assert.equal(review.effectiveSubmissionCount, 19);
|
|
27
|
+
assert.equal(review.submittedSinceLastReview, 10);
|
|
28
|
+
assert.equal(review.reviewDue, true);
|
|
29
|
+
assert.ok(review.reviewReasons.includes('submission-hygiene'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('late failure does not delay mature review while conversion still uses effective applications', () => {
|
|
33
|
+
const entries = applications(40, '2026-01-01T10:00:00Z');
|
|
34
|
+
const acknowledgements = [{ uniqueSubmissionCount: 40, maturedApplicationCount: 20 }];
|
|
35
|
+
const outcomes = [{ id: 'application-1', status: 'interview', occurredAt: '2026-02-01T10:00:00Z' }];
|
|
36
|
+
const review = buildReview(entries, outcomes, acknowledgements, now, [failure]);
|
|
37
|
+
assert.equal(review.maturedApplications, 39);
|
|
38
|
+
assert.equal(review.conversionRates.interview, 2.6);
|
|
39
|
+
assert.equal(review.reviewDue, true);
|
|
40
|
+
assert.deepEqual(review.reviewReasons, ['outcome-effectiveness']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('review acknowledgement checkpoints recorded canonical counts after delivery failure', async (t) => {
|
|
44
|
+
const dir = await mkdtemp(join(tmpdir(), 'review-cadence-'));
|
|
45
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
46
|
+
const entries = applications(20, '2026-01-01T10:00:00Z');
|
|
47
|
+
// A duplicate row must not inflate either checkpoint.
|
|
48
|
+
entries.push({ ...entries[1], id: 'duplicate-row' });
|
|
49
|
+
await writeFile(join(dir, 'applications.ndjson'), entries.map(JSON.stringify).join('\n') + '\n');
|
|
50
|
+
await writeFile(join(dir, 'delivery.ndjson'), JSON.stringify(failure) + '\n');
|
|
51
|
+
await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
|
|
52
|
+
const env = {
|
|
53
|
+
...process.env, JOB_APPLICATION_AGENT_STATE_DIR: dir,
|
|
54
|
+
JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json'),
|
|
55
|
+
JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
|
|
56
|
+
};
|
|
57
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
58
|
+
const ack = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review-ack', '--stdin'], {
|
|
59
|
+
env, encoding: 'utf8', input: JSON.stringify({ reviewedAt: now.toISOString() }),
|
|
60
|
+
}));
|
|
61
|
+
assert.equal(ack.uniqueSubmissionCount, 20);
|
|
62
|
+
assert.equal(ack.maturedApplicationCount, 20);
|
|
63
|
+
const stored = JSON.parse((await readFile(join(dir, 'reviews.ndjson'), 'utf8')).trim());
|
|
64
|
+
assert.equal(stored.uniqueSubmissionCount, 20);
|
|
65
|
+
assert.equal(stored.maturedApplicationCount, 20);
|
|
66
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm } 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 {
|
|
8
|
+
FILL_DISPLAY,
|
|
9
|
+
FILL_VNC_PORT,
|
|
10
|
+
FORBIDDEN_VNC_PORT,
|
|
11
|
+
assertSameTab,
|
|
12
|
+
createSessionBinding,
|
|
13
|
+
extractSessionBindingFields,
|
|
14
|
+
readSessionBindingFile,
|
|
15
|
+
sessionBindingPath,
|
|
16
|
+
validateSessionBinding,
|
|
17
|
+
writeSessionBindingFile,
|
|
18
|
+
} from "../scripts/session-binding.mjs";
|
|
19
|
+
|
|
20
|
+
test("validateSessionBinding requires fill display and VNC 5900", () => {
|
|
21
|
+
const ok = validateSessionBinding({
|
|
22
|
+
attentionId: "attention-1",
|
|
23
|
+
jobUrl: "https://jobs.ashbyhq.com/acme/application",
|
|
24
|
+
browserProfilePath: "/tmp/jaa-chrome",
|
|
25
|
+
});
|
|
26
|
+
assert.equal(ok.ok, true);
|
|
27
|
+
assert.equal(ok.binding.display, FILL_DISPLAY);
|
|
28
|
+
assert.equal(ok.binding.vncPort, FILL_VNC_PORT);
|
|
29
|
+
|
|
30
|
+
const badPort = validateSessionBinding({
|
|
31
|
+
attentionId: "attention-1",
|
|
32
|
+
jobUrl: "https://jobs.example.com/a",
|
|
33
|
+
browserProfilePath: "/tmp/p",
|
|
34
|
+
vncPort: FORBIDDEN_VNC_PORT,
|
|
35
|
+
});
|
|
36
|
+
assert.equal(badPort.ok, false);
|
|
37
|
+
assert.match(badPort.error, /5901/);
|
|
38
|
+
|
|
39
|
+
const badDisplay = validateSessionBinding({
|
|
40
|
+
attentionId: "attention-1",
|
|
41
|
+
jobUrl: "https://jobs.example.com/a",
|
|
42
|
+
browserProfilePath: "/tmp/p",
|
|
43
|
+
display: ":1",
|
|
44
|
+
});
|
|
45
|
+
assert.equal(badDisplay.ok, false);
|
|
46
|
+
assert.match(badDisplay.error, /forbidden|display/i);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("assertSameTab detects drift and accepts same application path", () => {
|
|
50
|
+
const same = assertSameTab(
|
|
51
|
+
"https://jobs.ashbyhq.com/acme/job/123",
|
|
52
|
+
"https://jobs.ashbyhq.com/acme/job/123?utm_source=x",
|
|
53
|
+
);
|
|
54
|
+
assert.equal(same.ok, true);
|
|
55
|
+
|
|
56
|
+
const drift = assertSameTab(
|
|
57
|
+
"https://jobs.ashbyhq.com/acme/job/123",
|
|
58
|
+
"https://jobs.ashbyhq.com/acme",
|
|
59
|
+
);
|
|
60
|
+
assert.equal(drift.ok, false);
|
|
61
|
+
assert.equal(drift.reason, "tab_drift");
|
|
62
|
+
|
|
63
|
+
const hintFail = assertSameTab(
|
|
64
|
+
"https://jobs.ashbyhq.com/acme/job/123",
|
|
65
|
+
"https://jobs.ashbyhq.com/acme/job/123",
|
|
66
|
+
{ urlContains: "/application" },
|
|
67
|
+
);
|
|
68
|
+
assert.equal(hintFail.ok, false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("write/read session binding stays local under state dir", async (t) => {
|
|
72
|
+
const dir = await mkdtemp(join(tmpdir(), "jaa-binding-"));
|
|
73
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
74
|
+
|
|
75
|
+
const binding = createSessionBinding({
|
|
76
|
+
attentionId: "attention-abc",
|
|
77
|
+
jobUrl: "https://jobs.ashbyhq.com/acme/application",
|
|
78
|
+
browserProfilePath: "/home/runner/.jaa-chrome-fill",
|
|
79
|
+
applicationId: "app-1",
|
|
80
|
+
roundId: "round-1",
|
|
81
|
+
tabHint: { urlContains: "/application" },
|
|
82
|
+
});
|
|
83
|
+
const path = sessionBindingPath(dir, binding.attentionId);
|
|
84
|
+
await writeSessionBindingFile(path, binding);
|
|
85
|
+
const loaded = await readSessionBindingFile(path);
|
|
86
|
+
assert.equal(loaded.attentionId, "attention-abc");
|
|
87
|
+
assert.equal(loaded.vncPort, 5900);
|
|
88
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
89
|
+
assert.equal(raw.browserProfilePath, "/home/runner/.jaa-chrome-fill");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("extractSessionBindingFields pulls local-only keys", () => {
|
|
93
|
+
const fields = extractSessionBindingFields({
|
|
94
|
+
url: "https://example.com",
|
|
95
|
+
browserProfilePath: "/tmp/p",
|
|
96
|
+
display: ":99",
|
|
97
|
+
vncPort: 5900,
|
|
98
|
+
});
|
|
99
|
+
assert.equal(fields.browserProfilePath, "/tmp/p");
|
|
100
|
+
assert.equal(fields.display, ":99");
|
|
101
|
+
assert.equal(extractSessionBindingFields({ url: "https://example.com" }), null);
|
|
102
|
+
});
|
|
@@ -19,14 +19,39 @@ test('documents durable autonomy, resumable rounds, attention and friction contr
|
|
|
19
19
|
const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
|
|
20
20
|
const autonomy = await readFile(new URL('../references/AUTONOMY.md', import.meta.url), 'utf8');
|
|
21
21
|
const runs = await readFile(new URL('../references/RUNS.md', import.meta.url), 'utf8');
|
|
22
|
+
const agentBox = await readFile(new URL('../references/agent-box/README.md', import.meta.url), 'utf8');
|
|
22
23
|
|
|
23
24
|
assert.match(skill, /autonomy status/);
|
|
24
25
|
assert.match(skill, /round status/);
|
|
25
26
|
assert.match(skill, /attention list/);
|
|
26
27
|
assert.match(skill, /friction record/);
|
|
28
|
+
assert.match(skill, /attention-runner-poll/);
|
|
29
|
+
assert.match(skill, /attention-resume-submit/);
|
|
30
|
+
assert.match(skill, /session-binding/);
|
|
31
|
+
assert.match(skill, /DISPLAY=:99/);
|
|
32
|
+
assert.match(skill, /5900/);
|
|
27
33
|
assert.match(autonomy, /never.*merge.*publish/i);
|
|
28
34
|
assert.match(autonomy, /CAPTCHA/i);
|
|
29
35
|
assert.match(runs, /visible.*confirmation/i);
|
|
30
36
|
assert.match(runs, /discoverySource/);
|
|
31
37
|
assert.match(runs, /applicationChannel/);
|
|
38
|
+
assert.match(runs, /Resume → submit/);
|
|
39
|
+
assert.match(runs, /attention-runner-poll/);
|
|
40
|
+
assert.match(runs, /submit if possible/i);
|
|
41
|
+
assert.match(agentBox, /localhost:5900/);
|
|
42
|
+
assert.match(agentBox, /5901/);
|
|
43
|
+
assert.match(agentBox, /DISPLAY=:99/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('documents Free.ai as optional LLM assist only', async () => {
|
|
47
|
+
const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
|
|
48
|
+
const freeAi = await readFile(new URL('../references/FREE_AI.md', import.meta.url), 'utf8');
|
|
49
|
+
|
|
50
|
+
assert.match(skill, /FREE_AI\.md/);
|
|
51
|
+
assert.match(skill, /does not call Free\.ai/i);
|
|
52
|
+
assert.match(freeAi, /https:\/\/api\.free\.ai\/v1/);
|
|
53
|
+
assert.match(freeAi, /FREE_AI_API_KEY/);
|
|
54
|
+
assert.match(freeAi, /qwen7b/);
|
|
55
|
+
assert.match(freeAi, /not the hosted/i);
|
|
56
|
+
assert.match(freeAi, /does not read `FREE_AI_API_KEY`/i);
|
|
32
57
|
});
|
|
@@ -71,6 +71,11 @@ function submission(index, roundId, overrides = {}) {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function recordLeads(env, roundId, sourceId, entries, otherCount = 0) {
|
|
75
|
+
for (const app of entries) cli(env, ['round','lead','--stdin'], { roundId, sourceId, applicationId: app.id, url: app.url, employerJobId: app.employerJobId, company: app.company, role: app.role, disposition:'qualified', observedAt:app.submittedAt, evidence:'Synthetic role meets the target requirements.' });
|
|
76
|
+
for (let i=0;i<otherCount;i++) cli(env, ['round','lead','--stdin'], { roundId, sourceId, url:`https://other.fixture.example/${sourceId}/${i}`, company:`Other ${i}`, disposition:'no-relevant-opening', observedAt:'2026-08-01T00:00:00Z', evidence:'No target role on this careers page.' });
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
test('persists a scoped autonomy grant and revokes future routine transmissions', async (t) => {
|
|
75
80
|
const { directory, env } = await fixture(t, 'autonomy');
|
|
76
81
|
|
|
@@ -122,6 +127,7 @@ test('counts only unique confirmed ledger submissions for an explicit round', as
|
|
|
122
127
|
assert.equal(status.completed, false);
|
|
123
128
|
|
|
124
129
|
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring']) {
|
|
130
|
+
recordLeads(env, started.roundId, sourceId, sourceId === 'linkedin-jobs-feed' ? Array.from({ length:30 },(_,i)=>submission(i,started.roundId)) : [], sourceId === 'linkedin-jobs-feed' ? 0 : 30);
|
|
125
131
|
cli(env, ['round', 'source', '--stdin'], {
|
|
126
132
|
roundId: started.roundId, sourceId, status: 'searched', reviewedCount: 30, qualifiedCount: sourceId === 'linkedin-jobs-feed' ? 30 : 0,
|
|
127
133
|
evidence: 'Reviewed matching postings against the unchanged target; other leads did not qualify.',
|
|
@@ -142,6 +148,8 @@ test('round completion requires distinct source coverage and explains concentrat
|
|
|
142
148
|
const { roundId } = cli(env, ['round', 'start', '--stdin'], { requestedCount: 2 });
|
|
143
149
|
for (const i of [1, 2]) cli(env, ['ledger', 'add', '--stdin'], submission(i, roundId, { discoverySourceId: 'linkedin-jobs-feed' }));
|
|
144
150
|
assert.match(cliFailure(env, ['round', 'complete', '--stdin'], { roundId }).stderr, /3 distinct discovery sources/i);
|
|
151
|
+
recordLeads(env,roundId,'linkedin-jobs-feed',[submission(1,roundId),submission(2,roundId)],8);
|
|
152
|
+
recordLeads(env,roundId,'indeed',[],10);
|
|
145
153
|
const check = { roundId, sourceId: 'linkedin-jobs-feed', status: 'searched', reviewedCount: 10, qualifiedCount: 2, evidence: 'Reviewed ten relevant postings using the target constraints.' };
|
|
146
154
|
cli(env, ['round', 'source', '--stdin'], check);
|
|
147
155
|
cli(env, ['round', 'source', '--stdin'], check);
|
|
@@ -169,7 +177,10 @@ test('discovery coverage rejects invalid reports and requires attribution withou
|
|
|
169
177
|
for (const bad of [{ sourceId: 'imaginary-board' }, { qualifiedCount: 5 }, { status: 'blocked' }, { applicationIds: ['not-in-round'] }, { evidence: '' }, { privateProfile: 'forbidden' }]) {
|
|
170
178
|
assert.equal(cliFailure(env, ['round', 'source', '--stdin'], { ...check, ...bad }).status, 1);
|
|
171
179
|
}
|
|
172
|
-
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring'])
|
|
180
|
+
for (const sourceId of ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring']) {
|
|
181
|
+
recordLeads(env,roundId,sourceId,[submission(1,roundId)],3);
|
|
182
|
+
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId });
|
|
183
|
+
}
|
|
173
184
|
assert.match(cliFailure(env, ['round', 'complete', '--stdin'], { roundId }).stderr, /attribution/i);
|
|
174
185
|
cli(env, ['round', 'source', '--stdin'], { ...check, applicationIds: ['round-role-1'] });
|
|
175
186
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.unattributedCount, 0);
|
|
@@ -180,7 +191,11 @@ test('source diversity is independent of ATS and concentration explanations are
|
|
|
180
191
|
const { env } = await fixture(t, 'balanced-discovery');
|
|
181
192
|
const { roundId } = cli(env, ['round', 'start', '--stdin'], { requestedCount: 5 });
|
|
182
193
|
const ids = ['linkedin-jobs-feed', 'indeed', 'hacker-news-who-is-hiring'];
|
|
183
|
-
for (const sourceId of ids)
|
|
194
|
+
for (const sourceId of ids) {
|
|
195
|
+
const entries=Array.from({length:5},(_,i)=>submission(i+1,roundId)).filter((_,i)=>ids[i%3]===sourceId);
|
|
196
|
+
recordLeads(env,roundId,sourceId,entries,1);
|
|
197
|
+
cli(env, ['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', evidence: 'Reviewed relevant postings and verified target fit.' });
|
|
198
|
+
}
|
|
184
199
|
for (let i = 1; i <= 5; i++) cli(env, ['ledger', 'add', '--stdin'], submission(i, roundId, { discoverySourceId: ids[(i - 1) % 3], applicationChannel: 'ashby' }));
|
|
185
200
|
const completed = cli(env, ['round', 'complete', '--stdin'], { roundId });
|
|
186
201
|
assert.equal(completed.discovery.maxSourceSharePercent, 40);
|
|
@@ -197,6 +212,7 @@ test('blocked-only attempts and two views of the same network do not satisfy dis
|
|
|
197
212
|
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId: 'indeed' });
|
|
198
213
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.coverageSatisfied, false);
|
|
199
214
|
const { blocker, ...searched } = check;
|
|
215
|
+
recordLeads(env,roundId,'indeed',[submission(1,roundId)]);
|
|
200
216
|
cli(env, ['round', 'source', '--stdin'], { ...searched, sourceId: 'indeed', status: 'searched', reviewedCount: 1, qualifiedCount: 1 });
|
|
201
217
|
cli(env, ['round', 'source', '--stdin'], { ...check, sourceId: 'indeed' });
|
|
202
218
|
assert.equal(cli(env, ['round', 'status', roundId]).discovery.coverageSatisfied, true);
|
|
@@ -723,7 +739,18 @@ test('replays and prioritizes an owner-only attention queue', async (t) => {
|
|
|
723
739
|
stage: 'submission',
|
|
724
740
|
blocker: 'captcha',
|
|
725
741
|
requiredActions: ['complete-captcha'],
|
|
726
|
-
|
|
742
|
+
browserProfilePath: join(directory, 'chrome-fill'),
|
|
743
|
+
display: ':99',
|
|
744
|
+
vncPort: 5900,
|
|
745
|
+
tabHint: { urlContains: '/captcha' },
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
assert.ok(captcha.sessionBinding?.binding?.browserProfilePath);
|
|
749
|
+
assert.equal(captcha.sessionBinding.binding.vncPort, 5900);
|
|
750
|
+
assert.equal(captcha.sessionBinding.binding.display, ':99');
|
|
751
|
+
const attentionRaw = await readFile(join(directory, 'attention.ndjson'), 'utf8');
|
|
752
|
+
assert.doesNotMatch(attentionRaw, /browserProfilePath/);
|
|
753
|
+
assert.ok((await readFile(captcha.sessionBinding.path, 'utf8')).includes('chrome-fill'));
|
|
727
754
|
|
|
728
755
|
const before = cli(env, ['attention', 'list']);
|
|
729
756
|
assert.deepEqual(before.items.map((item) => item.id), [captcha.id, judgment.id]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-application-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
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",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test": "node --test",
|
|
44
44
|
"check:native-artifacts": "node scripts/ci/validate-native-artifacts.mjs",
|
|
45
|
-
"privacy-audit": "node --test job-application-agent/tests/privacy-audit.test.mjs",
|
|
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/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",
|
|
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",
|
|
48
48
|
"prepack": "npm run check && npm test && npm run privacy-audit"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
@@ -52,5 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"yaml": "2.9.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"sql.js": "1.13.0"
|
|
55
58
|
}
|
|
56
59
|
}
|