@marcomigozzi/trailhead 0.16.1 → 0.18.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.it.md +82 -25
- package/README.md +82 -25
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +155 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +28 -19
- package/bin/lib/codex-projection.test.js +77 -20
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +15 -8
- package/bin/trailhead.test.js +71 -17
- package/bin/unblock-docs.test.js +56 -0
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +42 -17
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
- package/plugins/trailhead/skills/_shared/substrate.md +4 -3
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques.md +8 -6
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +11 -6
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +419 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Regression (#349): an inherited GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE must not
|
|
2
|
+
// redirect the plugin's git spawns away from the checkout they are pointed at.
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const T = path.resolve(__dirname, '..', '..', 'templates');
|
|
10
|
+
let passed = 0;
|
|
11
|
+
const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; console.log('ok -', name); };
|
|
12
|
+
|
|
13
|
+
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'git-env-spawn-')));
|
|
14
|
+
const cleanEnv = () => { const e = { ...process.env }; for (const k of Object.keys(e)) if (/^GIT_(DIR|WORK_TREE|INDEX_FILE|COMMON_DIR)$/.test(k)) delete e[k]; return e; };
|
|
15
|
+
const git = (cwd, ...args) => {
|
|
16
|
+
const r = spawnSync('git', ['-C', cwd, '-c', 'user.name=t', '-c', 'user.email=t@t', ...args], { encoding: 'utf8', env: cleanEnv() });
|
|
17
|
+
assert.strictEqual(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
18
|
+
return r.stdout.trim();
|
|
19
|
+
};
|
|
20
|
+
function makeRepo(name) {
|
|
21
|
+
const dir = path.join(tmp, name);
|
|
22
|
+
fs.mkdirSync(dir);
|
|
23
|
+
git(dir, 'init', '-q', '-b', 'main');
|
|
24
|
+
git(dir, 'commit', '-q', '--allow-empty', '-m', 'init');
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
const a = makeRepo('a');
|
|
28
|
+
const b = makeRepo('b');
|
|
29
|
+
git(b, 'checkout', '-q', '-b', 'trailhead/t5');
|
|
30
|
+
git(b, 'commit', '-q', '--allow-empty', '-m', 'feat: work\n\nRefs: #5');
|
|
31
|
+
git(b, 'checkout', '-q', 'main');
|
|
32
|
+
const aHead = git(a, 'rev-parse', 'HEAD');
|
|
33
|
+
const bMain = git(b, 'rev-parse', 'main');
|
|
34
|
+
const bBranch = git(b, 'rev-parse', 'trailhead/t5');
|
|
35
|
+
|
|
36
|
+
// The hostile parent env: points every git call at a.
|
|
37
|
+
const hostile = { ...process.env, GIT_DIR: path.join(a, '.git'), GIT_WORK_TREE: a, GIT_INDEX_FILE: path.join(a, '.git', 'index') };
|
|
38
|
+
const node = (script, args, env) => spawnSync(process.execPath, [path.join(T, script), ...args], { encoding: 'utf8', env });
|
|
39
|
+
|
|
40
|
+
// checkout-in: a plain dir is no repo; with GIT_DIR set it must still be invalid.
|
|
41
|
+
const plain = path.join(tmp, 'plain');
|
|
42
|
+
fs.mkdirSync(plain);
|
|
43
|
+
const ci = node('trailhead-checkout-in.js', ['--in', plain, '--cwd', plain], hostile);
|
|
44
|
+
eq('checkout-in does not resolve a plain dir through GIT_DIR', JSON.parse(ci.stdout.trim()).status, 'invalid');
|
|
45
|
+
|
|
46
|
+
// auto-merge: merges into b, leaves a alone.
|
|
47
|
+
const am = node('trailhead-auto-merge.js', ['merge', '--repo', b, '--ticket', '5'], hostile);
|
|
48
|
+
const out = JSON.parse(am.stdout.trim());
|
|
49
|
+
eq('auto-merge merges into the target repo', [out.status, git(b, 'rev-parse', 'main')], ['merged', bBranch]);
|
|
50
|
+
eq('auto-merge leaves the GIT_DIR repo alone', git(a, 'rev-parse', 'HEAD'), aHead);
|
|
51
|
+
assert.notStrictEqual(bMain, bBranch);
|
|
52
|
+
|
|
53
|
+
// issue-body: the gh spawn (which asks git for the repo) must not see GIT_DIR.
|
|
54
|
+
const bin = path.join(tmp, 'bin');
|
|
55
|
+
fs.mkdirSync(bin);
|
|
56
|
+
fs.writeFileSync(path.join(bin, 'gh'), '#!/bin/sh\nprintf "dir=%s wt=%s idx=%s" "${GIT_DIR-unset}" "${GIT_WORK_TREE-unset}" "${GIT_INDEX_FILE-unset}"\n', { mode: 0o755 });
|
|
57
|
+
const probe = `process.stdout.write(require(${JSON.stringify(path.join(T, 'trailhead-issue-body.js'))}).defaultGh(['x']).stdout)`;
|
|
58
|
+
const ib = spawnSync(process.execPath, ['-e', probe], { encoding: 'utf8', env: { ...hostile, PATH: `${bin}${path.delimiter}${process.env.PATH}` } });
|
|
59
|
+
eq('issue-body gh spawn gets no repo-location vars', ib.stdout, 'dir=unset wt=unset idx=unset');
|
|
60
|
+
|
|
61
|
+
// The other gh-spawning templates share the same one-line scrub.
|
|
62
|
+
for (const f of ['unblock', 'uat-backfill', 'merge-ticket', 'needs-you', 'uat-followup', 'auto-queue', 'capture']) {
|
|
63
|
+
const src = fs.readFileSync(path.join(T, `trailhead-${f}.js`), 'utf8');
|
|
64
|
+
eq(`${f} gh spawn is scrubbed`, /spawnSync\('gh', args, \{[^}]*env: gitEnv\(\)/.test(src), true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The statusline keeps its own copy of the names: it must equal the template list.
|
|
68
|
+
const sl = fs.readFileSync(path.join(T, 'statusline-trailhead.sh'), 'utf8');
|
|
69
|
+
const unsetLine = sl.match(/^unset ((?:GIT_\w+[ \\\n]+)+)/m)[1].split(/[\s\\]+/).filter(Boolean);
|
|
70
|
+
eq('statusline unset list equals the template list', [...unsetLine].sort(), [...require(path.join(T, 'trailhead-git-env.js')).GIT_LOCATION_VARS].sort());
|
|
71
|
+
|
|
72
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
73
|
+
console.log(`${passed} passed`);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Tests of templates/trailhead-git-env.js (and its hooks/lib copy).
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { GIT_LOCATION_VARS, gitEnv } = require('../../templates/trailhead-git-env.js');
|
|
7
|
+
|
|
8
|
+
let n = 0;
|
|
9
|
+
const t = (name, fn) => { fn(); n++; console.log('ok -', name); };
|
|
10
|
+
|
|
11
|
+
t('removes every repo-location name', () => {
|
|
12
|
+
const base = { PATH: '/bin' };
|
|
13
|
+
for (const k of GIT_LOCATION_VARS) base[k] = 'x';
|
|
14
|
+
assert.deepStrictEqual(gitEnv(base), { PATH: '/bin' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
t('keeps GIT_CONFIG* and unrelated names', () => {
|
|
18
|
+
const base = { GIT_DIR: 'x', GIT_CONFIG: 'a', GIT_CONFIG_PARAMETERS: 'b', GIT_CONFIG_COUNT: '1', HOME: '/h' };
|
|
19
|
+
assert.deepStrictEqual(gitEnv(base), { GIT_CONFIG: 'a', GIT_CONFIG_PARAMETERS: 'b', GIT_CONFIG_COUNT: '1', HOME: '/h' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
t('extra is applied on top and may re-add a name', () => {
|
|
23
|
+
assert.deepStrictEqual(gitEnv({ GIT_DIR: 'x', A: '1' }, { A: '2', GIT_EDITOR: 'true' }), { A: '2', GIT_EDITOR: 'true' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
t('does not mutate its input or process.env', () => {
|
|
27
|
+
const base = Object.freeze({ GIT_DIR: 'x' });
|
|
28
|
+
gitEnv(base);
|
|
29
|
+
assert.strictEqual(base.GIT_DIR, 'x');
|
|
30
|
+
process.env.GIT_DIR = '/nonexistent-for-test';
|
|
31
|
+
try {
|
|
32
|
+
assert.ok(!('GIT_DIR' in gitEnv()));
|
|
33
|
+
assert.strictEqual(process.env.GIT_DIR, '/nonexistent-for-test');
|
|
34
|
+
} finally { delete process.env.GIT_DIR; }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
t('the name list is git\'s local env vars minus GIT_CONFIG*', () => {
|
|
38
|
+
const r = spawnSync('git', ['rev-parse', '--local-env-vars'], { encoding: 'utf8' });
|
|
39
|
+
if (r.status !== 0) return;
|
|
40
|
+
const expected = r.stdout.split('\n').filter((s) => s && !s.startsWith('GIT_CONFIG')).sort();
|
|
41
|
+
const missing = expected.filter((k) => !GIT_LOCATION_VARS.includes(k));
|
|
42
|
+
assert.deepStrictEqual(missing, [], 'git knows local env vars the list lacks');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
t('hooks/lib copy is byte-identical to the template', () => {
|
|
46
|
+
const a = fs.readFileSync(path.join(__dirname, '..', '..', 'templates', 'trailhead-git-env.js'));
|
|
47
|
+
const b = fs.readFileSync(path.join(__dirname, 'trailhead-git-env.js'));
|
|
48
|
+
assert.ok(a.equals(b));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(`${n} passed`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// La config globale su Codex sta in <codexHome>, non in ~/.claude: ogni lettore, copiato dove l'installer
|
|
3
|
+
// Codex lo mette, deve trovarla lì. Run: node global-config-dir.test.js
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const pluginDir = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const READERS = [
|
|
11
|
+
['templates', 'trailhead-base-preflight.js'],
|
|
12
|
+
['templates', 'trailhead-inbox-needs-info.js'],
|
|
13
|
+
['templates', 'trailhead-isolation-copy.js'],
|
|
14
|
+
['templates', 'trailhead-plan-review-rounds.js'],
|
|
15
|
+
['templates', 'trailhead-test-run.js'],
|
|
16
|
+
['templates', 'trailhead-uat-setup.js'],
|
|
17
|
+
['hooks', 'trailhead-test-guard.js'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'trailhead-global-config-'));
|
|
21
|
+
const codexHome = path.join(tmp, 'codex');
|
|
22
|
+
const skillDir = path.join(codexHome, 'skills', 'trailhead');
|
|
23
|
+
const ENV = { HOME: path.join(tmp, 'home'), CLAUDE_CONFIG_DIR: path.join(tmp, 'claude') };
|
|
24
|
+
|
|
25
|
+
// Il percorso del file di config che il lettore userebbe, con l'env dato.
|
|
26
|
+
const configFileOf = (mod, env) =>
|
|
27
|
+
(mod.configDir ? path.join(mod.configDir(env), 'trailhead', 'config.json') : mod.globalConfigPath(env));
|
|
28
|
+
|
|
29
|
+
let passed = 0;
|
|
30
|
+
try {
|
|
31
|
+
for (const dir of ['templates', 'hooks']) {
|
|
32
|
+
fs.cpSync(path.join(pluginDir, dir), path.join(skillDir, dir), { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const expected = path.join(codexHome, 'trailhead', 'config.json');
|
|
35
|
+
for (const [dir, file] of READERS) {
|
|
36
|
+
const onCodex = require(path.join(skillDir, dir, file));
|
|
37
|
+
assert.strictEqual(configFileOf(onCodex, ENV), expected, `${file} on Codex`);
|
|
38
|
+
passed += 1;
|
|
39
|
+
|
|
40
|
+
const onClaude = require(path.join(pluginDir, dir, file));
|
|
41
|
+
assert.strictEqual(configFileOf(onClaude, ENV), path.join(ENV.CLAUDE_CONFIG_DIR, 'trailhead', 'config.json'), `${file} with CLAUDE_CONFIG_DIR`);
|
|
42
|
+
assert.strictEqual(configFileOf(onClaude, { HOME: ENV.HOME }), path.join(ENV.HOME, '.claude', 'trailhead', 'config.json'), `${file} default`);
|
|
43
|
+
passed += 2;
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
console.log(`global-config-dir.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Mod side of #369: the mod strips heredoc bodies before it tests a Bash call for a tracker write,
|
|
2
|
+
// with its own import-free leaf (a mod file imports nothing from hooks/lib). This keeps the leaf's
|
|
3
|
+
// output equal to shell-scan's stripHeredocBodies over the commands that matter to the write check.
|
|
4
|
+
// The mod file is TypeScript: loaded through Node's type stripping (Node >= 22.18);
|
|
5
|
+
// skipped (with an explicit SKIP line) on a Node with no type stripping; any other load error fails.
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { stripHeredocBodies } = require('./shell-scan.js');
|
|
10
|
+
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const t = (name, fn) => { fn(); passed++; console.log('ok -', name); };
|
|
13
|
+
|
|
14
|
+
const leaf = path.join(__dirname, '..', 'mod', 'heredoc.ts');
|
|
15
|
+
assert.ok(fs.existsSync(leaf), 'mod/heredoc.ts is missing');
|
|
16
|
+
let mod = null;
|
|
17
|
+
try {
|
|
18
|
+
mod = require(leaf);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
// Only a Node with no type stripping may skip; a syntax error in the leaf must fail the test.
|
|
21
|
+
const noStripping = err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || !process.features.typescript;
|
|
22
|
+
if (!noStripping) throw err;
|
|
23
|
+
console.log(`SKIP - mod/heredoc.ts not loadable without type stripping (${err.code || err.name})`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const COMMANDS = [
|
|
27
|
+
'cat >f <<EOF\ngh issue close 5\nEOF',
|
|
28
|
+
"cat >f <<'EOF'\ngh issue close 5\nEOF",
|
|
29
|
+
'cat >f <<"EOF"\ngh issue close 5\nEOF',
|
|
30
|
+
'cat >f <<-EOF\n\tgh issue close 5\n\tEOF',
|
|
31
|
+
'cat <<A >a; cat <<B >b\ngh issue close 1\nA\ngh issue edit 2\nB',
|
|
32
|
+
'cat <<A >a; cat <<B >b\nx\nA\ny\nB\ngh issue close 3',
|
|
33
|
+
'gh issue create --body-file - <<EOF\nbody\nEOF',
|
|
34
|
+
'gh issue comment 5 -F - <<EOF\ngh issue close 9\nEOF',
|
|
35
|
+
"cat <<'EOF' >x\nb\nEOF\ngh issue close 5",
|
|
36
|
+
"cat <<'EOF' >x\nEOF2\ngh issue close 5\n EOF\nEOF",
|
|
37
|
+
"cat <<'EOF' >x\ngh issue close 5",
|
|
38
|
+
'cat <<-EOF >x\nb\n EOF\ngh issue close 5\nEOF',
|
|
39
|
+
"cat <<< 'x'\ngh issue close 5",
|
|
40
|
+
'echo $((1<<2))\ngh issue close 5',
|
|
41
|
+
'echo $((1<<2)); gh issue view 5',
|
|
42
|
+
"cat <<'EOF' >x\nnode trailhead-issue-body.js append\nEOF",
|
|
43
|
+
'echo "a <<EOF"\ngh issue close 5',
|
|
44
|
+
'gh issue close 5',
|
|
45
|
+
// W1: a `<<` in a comment opens nothing
|
|
46
|
+
'# build body with <<EOF\ngh issue close 5',
|
|
47
|
+
'git commit -m "x" # see <<EOF\ngh issue close 5',
|
|
48
|
+
'echo a;# c <<EOF\ngh issue close 5',
|
|
49
|
+
'echo a&&(# c <<EOF\ngh issue close 5',
|
|
50
|
+
// a `#` inside a word is no comment
|
|
51
|
+
'cat a#b <<EOF\ngh issue close 5\nEOF',
|
|
52
|
+
'cat a#b <<EOF\ngh issue close 5',
|
|
53
|
+
// W2: an opener inside a multi-line quoted string opens nothing
|
|
54
|
+
'echo "a\ncat <<EOF\nb"\ngh issue close 5\nEOF',
|
|
55
|
+
"echo 'a\ncat <<EOF\nb'\ngh issue close 5",
|
|
56
|
+
'echo "a\n# x <<EOF\nb" <<EOF\ngh issue close 5\nEOF\ngh issue edit 2',
|
|
57
|
+
'echo "unbalanced\ncat <<EOF\ngh issue close 5\nEOF',
|
|
58
|
+
// a `<<` inside a parameter expansion opens nothing
|
|
59
|
+
'echo ${x:-<<EOF}\ngh issue close 5\nEOF',
|
|
60
|
+
'echo ${x:-<<EOF\ngh issue close 5',
|
|
61
|
+
// a body is not scanned for quotes: its apostrophe leaks nowhere
|
|
62
|
+
"cat <<EOF\nit's\nEOF\ncat <<B\ngh issue close 5\nB",
|
|
63
|
+
"cat <<EOF\nit's\nEOF\ngh issue close 5",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Known difference (I1): shell-scan sees an opener inside `$(..)` that the leaf does not, so the
|
|
67
|
+
// leaf keeps those lines (an extra read at worst). Asserted, not emulated.
|
|
68
|
+
const KNOWN_DIFFS = [
|
|
69
|
+
'echo "$(cat <<EOF\nhello\nEOF\n)"',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
if (mod) {
|
|
73
|
+
t('the mod leaf strips like shell-scan over the write-check commands', () => {
|
|
74
|
+
for (const cmd of COMMANDS) {
|
|
75
|
+
assert.strictEqual(mod.stripHeredocBodies(cmd), stripHeredocBodies(cmd), JSON.stringify(cmd));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
t('the known $(..) difference keep the lines (safe direction)', () => {
|
|
79
|
+
for (const cmd of KNOWN_DIFFS) {
|
|
80
|
+
assert.strictEqual(mod.stripHeredocBodies(cmd), cmd, JSON.stringify(cmd));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`${passed} passed${mod ? '' : ' (type stripping unavailable: leaf checks skipped)'}`);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-inbox-needs-info.js. Run: node inbox-needs-info.test.js
|
|
3
|
+
// No framework: plain asserts, same style as uat-backfill.test.js.
|
|
4
|
+
//
|
|
5
|
+
// Layers covered:
|
|
6
|
+
// 1. main() against a fake tracker behind an injected `gh` runner: the
|
|
7
|
+
// classification, the reporter match, the days config, failed reads.
|
|
8
|
+
// 2. End-to-end: the script itself with a fake `gh` on PATH (multi-page read,
|
|
9
|
+
// failing read).
|
|
10
|
+
const assert = require('assert');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-inbox-needs-info.js');
|
|
17
|
+
const { main } = require(scriptPath);
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
21
|
+
|
|
22
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'inbox-needs-info-'));
|
|
23
|
+
const NOW = new Date('2026-06-30T12:00:00Z');
|
|
24
|
+
const LABELED = '2026-06-10T12:00:00Z'; // 20 days before NOW
|
|
25
|
+
|
|
26
|
+
// issues: [{number,title,author,labeledAt?,labelEvents?,comments?,failEvents?,failComments?}]
|
|
27
|
+
function makeTracker(init = {}) {
|
|
28
|
+
const t = { issues: init.issues || [], failList: !!init.failList, calls: [] };
|
|
29
|
+
const res = (rows, status = 0) => ({ status, stdout: rows.map((r) => JSON.stringify(r)).join('\n'), stderr: status ? 'forced failure' : '' });
|
|
30
|
+
t.gh = (args) => {
|
|
31
|
+
t.calls.push(args.join(' '));
|
|
32
|
+
if (args[0] !== 'api') return res([], 1);
|
|
33
|
+
const url = args[2];
|
|
34
|
+
if (url.includes('/issues?labels=trailhead:needs-info&state=open&per_page=100')) {
|
|
35
|
+
if (t.failList) return res([], 1);
|
|
36
|
+
return res(t.issues.map((i) => ({ number: i.number, title: i.title || `T${i.number}`, author: i.author === undefined ? 'reporter' : i.author })));
|
|
37
|
+
}
|
|
38
|
+
const m = /\/issues\/(\d+)\/(events|comments)/.exec(url);
|
|
39
|
+
if (!m) return res([], 1);
|
|
40
|
+
const i = t.issues.find((x) => x.number === Number(m[1]));
|
|
41
|
+
if (m[2] === 'events') {
|
|
42
|
+
if (i.failEvents) return res([], 1);
|
|
43
|
+
return res(i.labelEvents || [{ at: i.labeledAt || LABELED }]);
|
|
44
|
+
}
|
|
45
|
+
if (i.failComments) return res([], 1);
|
|
46
|
+
return res(i.comments || []);
|
|
47
|
+
};
|
|
48
|
+
return t;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function run(t, { root, env, argv } = {}) {
|
|
52
|
+
let out = '';
|
|
53
|
+
const code = main(argv || ['--repo', 'o/r', '--config-root', root || tmp], {
|
|
54
|
+
gh: t.gh, now: () => NOW, env: env || { CLAUDE_CONFIG_DIR: path.join(tmp, 'noglobal') }, stdout: { write: (s) => { out += s; } }, stderr: { write: () => {} },
|
|
55
|
+
});
|
|
56
|
+
let json = null;
|
|
57
|
+
try { json = JSON.parse(out.trim()); } catch { /* not json */ }
|
|
58
|
+
return { code, out, json };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nums = (a) => a.map((x) => x.number);
|
|
62
|
+
|
|
63
|
+
// --- classification ----------------------------------------------------------
|
|
64
|
+
{
|
|
65
|
+
const t = makeTracker({ issues: [
|
|
66
|
+
{ number: 1, comments: [{ author: 'reporter', at: '2026-06-05T00:00:00Z' }] }, // before the label
|
|
67
|
+
{ number: 2, comments: [{ author: 'someone', at: '2026-06-11T00:00:00Z' }] }, // other user after
|
|
68
|
+
{ number: 3, comments: [{ author: 'reporter', at: '2026-06-11T00:00:00Z' }] }, // reporter after
|
|
69
|
+
{ number: 4, author: 'Reporter', comments: [{ author: 'reporter', at: '2026-06-12T00:00:00Z' }] }, // case-different login
|
|
70
|
+
{ number: 5, comments: [{ author: 'reporter', at: LABELED }] }, // same-second tie
|
|
71
|
+
{ number: 6, author: null, comments: [] }, // deleted reporter
|
|
72
|
+
{ number: 7, author: 'ghost', comments: [{ author: 'ghost', at: '2026-06-12T00:00:00Z' }] },
|
|
73
|
+
{ number: 8, labeledAt: '2026-06-25T12:00:00Z' }, // 5 days: still waiting
|
|
74
|
+
{ number: 9, labeledAt: '2026-06-16T12:00:00Z' }, // exactly 14 days: timed out
|
|
75
|
+
{ number: 10, labeledAt: '2026-06-16T12:00:01Z' }, // 13 whole days: waiting
|
|
76
|
+
{ number: 11, labelEvents: [] }, // no label event
|
|
77
|
+
{ number: 12, failEvents: true },
|
|
78
|
+
{ number: 13, failComments: true },
|
|
79
|
+
{ number: 14, labelEvents: [{ at: '2026-05-01T00:00:00Z' }, { at: '2026-06-25T12:00:00Z' }], comments: [{ author: 'reporter', at: '2026-06-01T00:00:00Z' }] }, // latest label wins
|
|
80
|
+
] });
|
|
81
|
+
const r = run(t);
|
|
82
|
+
ok('exit 0, one JSON line', r.code === 0 && r.json.status === 'ok' && r.out.trim().split('\n').length === 1);
|
|
83
|
+
ok('answered: reporter after the label, case-insensitive', JSON.stringify(nums(r.json.answered)) === '[3,4]');
|
|
84
|
+
ok('answered carries answeredAt and author', r.json.answered[0].answeredAt === '2026-06-11T00:00:00Z' && r.json.answered[0].author === 'reporter');
|
|
85
|
+
ok('timedOut: before-label comment, other user, tie, 14 whole days', JSON.stringify(nums(r.json.timedOut)) === '[1,2,5,9]');
|
|
86
|
+
ok('timedOut carries ageDays', r.json.timedOut.find((x) => x.number === 9).ageDays === 14);
|
|
87
|
+
ok('waiting stays out of answered and timedOut', JSON.stringify(nums(r.json.waiting)) === '[8,10,14]');
|
|
88
|
+
ok('unknown: deleted reporter, ghost, no event, failed reads', JSON.stringify(nums(r.json.unknown)) === '[6,7,11,12,13]' && r.json.unknown.every((u) => u.reason));
|
|
89
|
+
ok('the default is 14 days', r.json.days === 14 && r.json.warnings.length === 0);
|
|
90
|
+
ok('read-only: only api reads, pull requests filtered by jq', t.calls.every((c) => c.startsWith('api ')) && t.calls[0].includes('select(.pull_request == null)'));
|
|
91
|
+
ok('comments are read since the label', t.calls.some((c) => c.includes('/issues/3/comments?since=2026-06-10T12:00:00Z&per_page=100')));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- days config -------------------------------------------------------------
|
|
95
|
+
function configRoot(cfg) {
|
|
96
|
+
const root = fs.mkdtempSync(path.join(tmp, 'proj-'));
|
|
97
|
+
fs.mkdirSync(path.join(root, '.trailhead'));
|
|
98
|
+
if (cfg !== undefined) fs.writeFileSync(path.join(root, '.trailhead', 'config.json'), typeof cfg === 'string' ? cfg : JSON.stringify(cfg));
|
|
99
|
+
return root;
|
|
100
|
+
}
|
|
101
|
+
function globalEnv(cfg) {
|
|
102
|
+
const dir = fs.mkdtempSync(path.join(tmp, 'glob-'));
|
|
103
|
+
fs.mkdirSync(path.join(dir, 'trailhead'));
|
|
104
|
+
fs.writeFileSync(path.join(dir, 'trailhead', 'config.json'), typeof cfg === 'string' ? cfg : JSON.stringify(cfg));
|
|
105
|
+
return { CLAUDE_CONFIG_DIR: dir };
|
|
106
|
+
}
|
|
107
|
+
const one = () => makeTracker({ issues: [{ number: 1, labeledAt: '2026-06-25T12:00:00Z' }] }); // 5 days old
|
|
108
|
+
{
|
|
109
|
+
const r = run(one(), { root: configRoot({ 'inbox.needs_info_days': 3 }) });
|
|
110
|
+
ok('project override: flat key', r.json.days === 3 && r.json.timedOut.length === 1);
|
|
111
|
+
const n = run(one(), { root: configRoot({ inbox: { needs_info_days: 7 } }) });
|
|
112
|
+
ok('project override: nested key', n.json.days === 7 && n.json.waiting.length === 1);
|
|
113
|
+
const b = run(one(), { root: configRoot({ 'inbox.needs_info_days': 3, inbox: { needs_info_days: 9 } }) });
|
|
114
|
+
ok('flat key wins over nested in one file', b.json.days === 3);
|
|
115
|
+
const g = run(one(), { root: configRoot({}), env: globalEnv({ 'inbox.needs_info_days': 4 }) });
|
|
116
|
+
ok('global config applies when the project sets nothing', g.json.days === 4);
|
|
117
|
+
const p = run(one(), { root: configRoot({ 'inbox.needs_info_days': 2 }), env: globalEnv({ 'inbox.needs_info_days': 4 }) });
|
|
118
|
+
ok('project wins over global', p.json.days === 2);
|
|
119
|
+
}
|
|
120
|
+
for (const bad of [0, -1, 1.5, '7', null]) {
|
|
121
|
+
const r = run(one(), { root: configRoot({ 'inbox.needs_info_days': bad }), env: globalEnv({ 'inbox.needs_info_days': 6 }) });
|
|
122
|
+
ok(`invalid value ${JSON.stringify(bad)} warns and falls to the global layer`, r.json.days === 6 && r.json.warnings.length === 1);
|
|
123
|
+
}
|
|
124
|
+
{
|
|
125
|
+
const r = run(one(), { root: configRoot('{nope') });
|
|
126
|
+
ok('invalid JSON warns and falls to the default', r.json.days === 14 && r.json.warnings.length === 1);
|
|
127
|
+
const d = run(one(), { root: path.join(tmp, 'missing-root') });
|
|
128
|
+
ok('a missing project config is silent', d.json.days === 14 && d.json.warnings.length === 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- failures and usage ------------------------------------------------------
|
|
132
|
+
{
|
|
133
|
+
const r = run(makeTracker({ issues: [{ number: 1 }], failList: true }));
|
|
134
|
+
ok('a failed list read is status error, exit 1', r.code === 1 && r.json.status === 'error');
|
|
135
|
+
}
|
|
136
|
+
for (const argv of [['--bogus'], ['--repo'], ['--repo', 'nope', '--config-root', 'x'], ['--repo', 'o/r'], ['--config-root', 'x']]) {
|
|
137
|
+
const t = makeTracker();
|
|
138
|
+
const r = run(t, { argv });
|
|
139
|
+
ok(`usage error exit 2: ${argv.join(' ')}`, r.code === 2 && t.calls.length === 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- end-to-end with a fake gh on PATH ---------------------------------------
|
|
143
|
+
function e2e(script) {
|
|
144
|
+
const binDir = fs.mkdtempSync(path.join(tmp, 'bin-'));
|
|
145
|
+
fs.writeFileSync(path.join(binDir, 'gh'), script, { mode: 0o755 });
|
|
146
|
+
return spawnSync('node', [scriptPath, '--repo', 'o/r', '--config-root', tmp], { encoding: 'utf8', env: { ...process.env, CLAUDE_CONFIG_DIR: path.join(tmp, 'noglobal'), PATH: binDir + path.delimiter + process.env.PATH } });
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
// Two pages for each read: one JSON document per line, as `--jq ... | tojson` prints.
|
|
150
|
+
const good = e2e(`#!/bin/sh
|
|
151
|
+
case "$*" in
|
|
152
|
+
*"issues?labels="*) echo '{"number":5,"title":"A","author":"rep"}'; echo '{"number":6,"title":"B","author":"rep"}' ;;
|
|
153
|
+
*"/issues/5/events"*) echo '{"at":"2026-01-01T00:00:00Z"}'; echo '{"at":"2026-01-02T00:00:00Z"}' ;;
|
|
154
|
+
*"/issues/6/events"*) echo '{"at":"2026-01-01T00:00:00Z"}' ;;
|
|
155
|
+
*"/issues/5/comments"*) echo '{"author":"rep","at":"2026-01-03T00:00:00Z"}' ;;
|
|
156
|
+
*"/issues/6/comments"*) echo '{"author":"x","at":"2026-01-03T00:00:00Z"}' ;;
|
|
157
|
+
*) exit 1 ;;
|
|
158
|
+
esac
|
|
159
|
+
`);
|
|
160
|
+
const j = JSON.parse(good.stdout.trim());
|
|
161
|
+
ok('e2e: multi-page reads classify answered and timed out', good.status === 0 && JSON.stringify(nums(j.answered)) === '[5]' && JSON.stringify(nums(j.timedOut)) === '[6]');
|
|
162
|
+
const partial = e2e(`#!/bin/sh
|
|
163
|
+
case "$*" in
|
|
164
|
+
*"issues?labels="*) echo '{"number":5,"title":"A","author":"rep"}' ;;
|
|
165
|
+
*"/issues/5/events"*) echo '{"at":"2026-01-01T00:00:00Z"}' ;;
|
|
166
|
+
*"/issues/5/comments"*) echo '{"author":"rep","at":"2026-01-03T00:00:00Z"}'; exit 1 ;;
|
|
167
|
+
*) exit 1 ;;
|
|
168
|
+
esac
|
|
169
|
+
`);
|
|
170
|
+
const p = JSON.parse(partial.stdout.trim());
|
|
171
|
+
ok('e2e: a read that fails after printing is unknown, never classified', partial.status === 0 && nums(p.unknown).join() === '5' && p.answered.length === 0 && p.timedOut.length === 0);
|
|
172
|
+
const down = e2e('#!/bin/sh\nexit 1\n');
|
|
173
|
+
ok('e2e: gh failing exits 1 with one JSON error line', down.status === 1 && JSON.parse(down.stdout.trim()).status === 'error');
|
|
174
|
+
const bad = spawnSync('node', [scriptPath, '--bogus'], { encoding: 'utf8' });
|
|
175
|
+
ok('e2e: usage error exits 2', bad.status === 2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
179
|
+
console.log(`inbox-needs-info.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// inherit-session.js: l'unico riconoscimento di "inherit session". #323.
|
|
3
|
+
// Modulo senza dipendenze (nessun fs/os/path/require): lo usano sia il router
|
|
4
|
+
// (model-route.js) e la review dei pin (model-defaults-review.js), sia la
|
|
5
|
+
// proiezione Codex (bin/lib/codex-projection.js), che deve restare pura.
|
|
6
|
+
|
|
7
|
+
const INHERIT_SESSION = 'inherit session';
|
|
8
|
+
|
|
9
|
+
// Vale per una stringa nuda o per il campo `model` di un pin a oggetto, senza
|
|
10
|
+
// badare a spazi e maiuscole.
|
|
11
|
+
function isInheritSession(v) {
|
|
12
|
+
const isObject = !!v && typeof v === 'object' && !Array.isArray(v);
|
|
13
|
+
const id = isObject ? v.model : v;
|
|
14
|
+
return typeof id === 'string' && id.trim().toLowerCase() === INHERIT_SESSION;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { isInheritSession, INHERIT_SESSION };
|