jobhunt-kit 0.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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/AGENTS.md +23 -0
- package/CLAUDE.md +5 -0
- package/README.md +122 -0
- package/bin/jobhunt-kit.mjs +95 -0
- package/installer-assets/gitignore.txt +22 -0
- package/installer-assets/runtime-lock.json +492 -0
- package/installer-assets/workspace-lock.json +495 -0
- package/package.json +52 -0
- package/plugins/jobhunt-kit/.claude-plugin/plugin.json +9 -0
- package/plugins/jobhunt-kit/.codex-plugin/plugin.json +18 -0
- package/plugins/jobhunt-kit/THIRD_PARTY.md +18 -0
- package/plugins/jobhunt-kit/package-lock.json +492 -0
- package/plugins/jobhunt-kit/package.json +14 -0
- package/plugins/jobhunt-kit/references/cli.md +125 -0
- package/plugins/jobhunt-kit/references/cover-guidance.md +24 -0
- package/plugins/jobhunt-kit/references/hirify/LICENSE +202 -0
- package/plugins/jobhunt-kit/references/hirify/NOTICE +2 -0
- package/plugins/jobhunt-kit/references/hirify/SKILL.md +139 -0
- package/plugins/jobhunt-kit/references/hirify/reference.md +286 -0
- package/plugins/jobhunt-kit/references/matching.md +28 -0
- package/plugins/jobhunt-kit/references/resume-guidance.md +47 -0
- package/plugins/jobhunt-kit/references/storage.md +120 -0
- package/plugins/jobhunt-kit/references/workflow.md +89 -0
- package/plugins/jobhunt-kit/scripts/cli.mjs +9 -0
- package/plugins/jobhunt-kit/scripts/commands.mjs +157 -0
- package/plugins/jobhunt-kit/scripts/extract-resume.mjs +27 -0
- package/plugins/jobhunt-kit/scripts/hirify.mjs +31 -0
- package/plugins/jobhunt-kit/scripts/profile.mjs +79 -0
- package/plugins/jobhunt-kit/scripts/resume.mjs +92 -0
- package/plugins/jobhunt-kit/scripts/send-packet.mjs +36 -0
- package/plugins/jobhunt-kit/scripts/setup.mjs +25 -0
- package/plugins/jobhunt-kit/scripts/tracker.mjs +332 -0
- package/plugins/jobhunt-kit/skills/job-apply/SKILL.md +61 -0
- package/plugins/jobhunt-kit/skills/job-profile/SKILL.md +33 -0
- package/plugins/jobhunt-kit/skills/job-resume/SKILL.md +29 -0
- package/plugins/jobhunt-kit/skills/job-search/SKILL.md +39 -0
- package/plugins/jobhunt-kit/skills/job-track/SKILL.md +30 -0
- package/plugins/jobhunt-kit/templates/cover-letter.md +17 -0
- package/plugins/jobhunt-kit/templates/intake.md +62 -0
- package/plugins/jobhunt-kit/templates/policy.json +6 -0
- package/plugins/jobhunt-kit/templates/profile.json +29 -0
- package/plugins/jobhunt-kit/templates/profile.md +21 -0
- package/plugins/jobhunt-kit/templates/resume-review.md +30 -0
- package/plugins/jobhunt-kit/templates/scheduled-search.md +21 -0
- package/scripts/check-package.mjs +62 -0
- package/tests/commands.test.mjs +143 -0
- package/tests/installer.test.mjs +52 -0
- package/tests/tracker.test.mjs +180 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { Tracker, initData } from '../plugins/jobhunt-kit/scripts/tracker.mjs';
|
|
9
|
+
import { sendPacket } from '../plugins/jobhunt-kit/scripts/send-packet.mjs';
|
|
10
|
+
|
|
11
|
+
function fixture(t, ready = true) {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), 'job-search-test-'));
|
|
13
|
+
initData(dir);
|
|
14
|
+
const profilePath = join(dir, 'profile.json');
|
|
15
|
+
const profile = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
16
|
+
if (ready) {
|
|
17
|
+
profile.confirmed_at = '2026-01-01T00:00:00Z';
|
|
18
|
+
profile.search.roles = ['Fictional librarian'];
|
|
19
|
+
profile.hirify_profile_id = 123;
|
|
20
|
+
profile.evidence = [{ id: 'E1', text: 'Fictional library experience', confirmed: true, source: 'synthetic fixture' }];
|
|
21
|
+
writeFileSync(join(dir, 'resumes', 'fictional.txt'), 'Fictional resume for offline test only');
|
|
22
|
+
profile.resume = { path: 'resumes/fictional.txt', sha256: createHash('sha256').update(readFileSync(join(dir, 'resumes', 'fictional.txt'))).digest('hex'), review_status: 'ready' };
|
|
23
|
+
writeFileSync(profilePath, JSON.stringify(profile));
|
|
24
|
+
}
|
|
25
|
+
const store = new Tracker(dir);
|
|
26
|
+
t.after(() => {
|
|
27
|
+
store.close();
|
|
28
|
+
const absolute = resolve(dir);
|
|
29
|
+
assert.ok(absolute.startsWith(resolve(tmpdir()) + (process.platform === 'win32' ? '\\' : '/')) && absolute.includes('job-search-test-'));
|
|
30
|
+
rmSync(absolute, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
return { dir, store, profile, profilePath };
|
|
33
|
+
}
|
|
34
|
+
function vacancy(run, slug = 'fictional-a', route = 'hosted') {
|
|
35
|
+
return { run_id: run.run_id, slug, title: 'Fictional library role', url: `https://example.invalid/${slug}`,
|
|
36
|
+
route, description: 'Fictional vacancy, do not apply', read_at: '2026-01-01T00:00:00Z',
|
|
37
|
+
match: { verdict: 'suitable', reasons: ['Fictional evidence match'], requirements: [{ requirement: 'Library skill', result: 'pass', evidence: 'E1 (synthetic)' }] } };
|
|
38
|
+
}
|
|
39
|
+
function prepare(store, slug = 'fictional-a', route = 'hosted') {
|
|
40
|
+
const run = store.runStart({ query: 'fictional' });
|
|
41
|
+
store.put(vacancy(run, slug, route));
|
|
42
|
+
store.prepare({ slug, cover_letter: 'Fictional "cover"\n$(do not execute) `literal`', hirify_profile_id: 123,
|
|
43
|
+
claims: [{ text: 'Fictional library experience', evidence_id: 'E1' }], answers: [], unresolved: [] });
|
|
44
|
+
}
|
|
45
|
+
function auto(store, limit = 1) {
|
|
46
|
+
store.policy({ mode: 'auto', auto: { user_authorization: 'Fictional authorization for offline test only',
|
|
47
|
+
profile_hash: store.profile().hash, expires_at: new Date(Date.now() + 3600000).toISOString(), max_attempts_per_utc_day: limit } });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('empty template blocks search and preserves user edits on re-init', t => {
|
|
51
|
+
const f = fixture(t, false);
|
|
52
|
+
assert.throws(() => f.store.runStart({}), /confirm profile/);
|
|
53
|
+
assert.equal(JSON.parse(readFileSync(join(f.dir, 'policy.json'))).mode, 'review_each');
|
|
54
|
+
f.profile.identity.name = 'Fictional candidate';
|
|
55
|
+
writeFileSync(f.profilePath, JSON.stringify(f.profile));
|
|
56
|
+
initData(f.dir);
|
|
57
|
+
assert.equal(JSON.parse(readFileSync(f.profilePath)).identity.name, 'Fictional candidate');
|
|
58
|
+
});
|
|
59
|
+
test('canonical URL deduplication persists and reimport preserves draft status', t => {
|
|
60
|
+
const { store, dir } = fixture(t);
|
|
61
|
+
prepare(store);
|
|
62
|
+
const r = store.runStart({});
|
|
63
|
+
const x = vacancy(r, 'fictional-alias');
|
|
64
|
+
x.url = 'https://example.invalid/fictional-a?utm_source=test#fragment';
|
|
65
|
+
assert.equal(store.put(x).is_new, false);
|
|
66
|
+
assert.equal(store.list().length, 1);
|
|
67
|
+
assert.equal(store.job('fictional-alias').status, 'draft');
|
|
68
|
+
const reopened = new Tracker(dir);
|
|
69
|
+
assert.equal(reopened.list().length, 1);
|
|
70
|
+
reopened.close();
|
|
71
|
+
});
|
|
72
|
+
test('full text is required to assess fit', t => {
|
|
73
|
+
const { store } = fixture(t);
|
|
74
|
+
const x = vacancy(store.runStart({}));
|
|
75
|
+
delete x.description;
|
|
76
|
+
assert.throws(() => store.put(x), /Read full vacancy/);
|
|
77
|
+
});
|
|
78
|
+
test('manual approval required and draft changes revoke approval', t => {
|
|
79
|
+
const { store } = fixture(t);
|
|
80
|
+
prepare(store);
|
|
81
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }), /approval required/);
|
|
82
|
+
store.approve({ slug: 'fictional-a', user_authorization: 'Synthetic individual yes' });
|
|
83
|
+
store.prepare({ ...store.job('fictional-a').draft, cover_letter: 'Changed fictional letter' });
|
|
84
|
+
assert.equal(store.job('fictional-a').approval, null);
|
|
85
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }), /approval required/);
|
|
86
|
+
});
|
|
87
|
+
test('profile change invalidates existing approved packet', t => {
|
|
88
|
+
const f = fixture(t);
|
|
89
|
+
prepare(f.store);
|
|
90
|
+
f.store.approve({ slug: 'fictional-a', user_authorization: 'Synthetic yes' });
|
|
91
|
+
f.profile.identity.city = 'Fictional city';
|
|
92
|
+
writeFileSync(f.profilePath, JSON.stringify(f.profile));
|
|
93
|
+
assert.throws(() => f.store.begin({ slug: 'fictional-a' }), /stale/);
|
|
94
|
+
});
|
|
95
|
+
test('changed resume bytes block submission', t => {
|
|
96
|
+
const { store, dir } = fixture(t);
|
|
97
|
+
prepare(store);
|
|
98
|
+
store.approve({ slug: 'fictional-a', user_authorization: 'Synthetic yes' });
|
|
99
|
+
writeFileSync(join(dir, 'resumes', 'fictional.txt'), 'Changed');
|
|
100
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }), /Resume file changed/);
|
|
101
|
+
});
|
|
102
|
+
test('external route produces handoff and cannot begin', t => {
|
|
103
|
+
const { store } = fixture(t);
|
|
104
|
+
prepare(store, 'fictional-a', 'external');
|
|
105
|
+
assert.equal(store.job('fictional-a').status, 'needs_user');
|
|
106
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }));
|
|
107
|
+
store.status({ slug: 'fictional-a', status: 'submitted_external', note: 'Fictional user report' });
|
|
108
|
+
assert.equal(store.job('fictional-a').status, 'submitted_external');
|
|
109
|
+
});
|
|
110
|
+
test('auto daily cap is shared between connections and survives restart', t => {
|
|
111
|
+
const { store, dir } = fixture(t);
|
|
112
|
+
prepare(store); prepare(store, 'fictional-b'); auto(store);
|
|
113
|
+
store.begin({ slug: 'fictional-a' });
|
|
114
|
+
const second = new Tracker(dir);
|
|
115
|
+
try { assert.throws(() => second.begin({ slug: 'fictional-b' }), /daily attempt cap/); }
|
|
116
|
+
finally { second.close(); }
|
|
117
|
+
assert.equal(store.job('fictional-b').status, 'draft');
|
|
118
|
+
});
|
|
119
|
+
test('auto rejects unknown critical conditions and expiry', t => {
|
|
120
|
+
const { store, dir } = fixture(t);
|
|
121
|
+
prepare(store); auto(store);
|
|
122
|
+
const r = store.runStart({});
|
|
123
|
+
const v = vacancy(r); v.match.requirements[0].result = 'unknown';
|
|
124
|
+
store.put(v);
|
|
125
|
+
store.prepare({ ...store.job('fictional-a').draft });
|
|
126
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }), /all critical requirements/);
|
|
127
|
+
const policy = JSON.parse(readFileSync(join(dir, 'policy.json')));
|
|
128
|
+
policy.auto.expires_at = '2000-01-01T00:00:00Z';
|
|
129
|
+
writeFileSync(join(dir, 'policy.json'), JSON.stringify(policy));
|
|
130
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }), /expired/);
|
|
131
|
+
});
|
|
132
|
+
test('dispatch passes literal arguments once, with durable duplicate guard', t => {
|
|
133
|
+
const { store, dir } = fixture(t);
|
|
134
|
+
prepare(store);
|
|
135
|
+
store.approve({ slug: 'fictional-a', user_authorization: 'Synthetic yes' });
|
|
136
|
+
const packet = store.begin({ slug: 'fictional-a' });
|
|
137
|
+
let calls = 0;
|
|
138
|
+
const fake = (data, args) => {
|
|
139
|
+
calls++; assert.equal(args[6], packet.cover_letter); assert.equal(data, dir);
|
|
140
|
+
return { status: 0, stdout: '{"fictional_success":true}', stderr: '' };
|
|
141
|
+
};
|
|
142
|
+
sendPacket(dir, packet, fake);
|
|
143
|
+
assert.throws(() => sendPacket(dir, packet, fake), /UNIQUE/);
|
|
144
|
+
assert.equal(calls, 1);
|
|
145
|
+
assert.equal(store.job('fictional-a').status, 'submitting'); // Raw success is not invented confirmation.
|
|
146
|
+
store.finish({ attempt_id: packet.attempt_id, outcome: 'submitted', application_id: 'fictional-1', evidence: 'Synthetic response, reviewed' });
|
|
147
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }));
|
|
148
|
+
assert.match(store.report().content, /submitted/);
|
|
149
|
+
assert.ok(store.history().some(e => e.kind === 'send_result'));
|
|
150
|
+
});
|
|
151
|
+
test('timeout records uncertainty, blocks retry, permits evidence-based resolution', t => {
|
|
152
|
+
const { store, dir } = fixture(t);
|
|
153
|
+
prepare(store); auto(store);
|
|
154
|
+
const packet = store.begin({ slug: 'fictional-a' });
|
|
155
|
+
sendPacket(dir, packet, () => ({ status: null, error: new Error('Synthetic timeout') }));
|
|
156
|
+
assert.equal(store.job('fictional-a').status, 'submission_unknown');
|
|
157
|
+
assert.throws(() => store.begin({ slug: 'fictional-a' }));
|
|
158
|
+
store.resolveUnknown({ slug: 'fictional-a', outcome: 'failed', evidence: 'Synthetic user confirmation: not sent' });
|
|
159
|
+
assert.equal(store.job('fictional-a').status, 'failed');
|
|
160
|
+
assert.throws(() => store.prepare({ slug: 'fictional-a' }), /cannot be drafted/);
|
|
161
|
+
});
|
|
162
|
+
test('auto revocation between intent and dispatch blocks network', t => {
|
|
163
|
+
const { store, dir } = fixture(t);
|
|
164
|
+
prepare(store); auto(store);
|
|
165
|
+
const packet = store.begin({ slug: 'fictional-a' });
|
|
166
|
+
store.policy({ mode: 'review_each' });
|
|
167
|
+
let called = false;
|
|
168
|
+
assert.throws(() => sendPacket(dir, packet, () => { called = true; }), /revoked/);
|
|
169
|
+
assert.equal(called, false);
|
|
170
|
+
});
|
|
171
|
+
test('CLI report and run history work from a fresh process', t => {
|
|
172
|
+
const { store, dir } = fixture(t);
|
|
173
|
+
const run = store.runStart({ query: 'synthetic' });
|
|
174
|
+
store.runEvent({ run_id: run.run_id, kind: 'preview', data: { count: 0 } });
|
|
175
|
+
store.runFinish({ run_id: run.run_id, status: 'complete', reason: 'No fictional matches' });
|
|
176
|
+
const cli = resolve('plugins/jobhunt-kit/scripts/tracker.mjs');
|
|
177
|
+
const r = spawnSync(process.execPath, [cli, '--data', dir, 'report'], { encoding: 'utf8' });
|
|
178
|
+
assert.equal(r.status, 0, r.stderr);
|
|
179
|
+
assert.match(JSON.parse(r.stdout).content, /No fictional matches/);
|
|
180
|
+
});
|