claude-dev-env 7.5.0 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/session-continuity/README.md +19 -9
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install-session-continuity.mjs +73 -30
- package/bin/install-session-continuity.test.mjs +52 -1
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +80 -15
- package/bin/install.test.mjs +57 -0
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/pr_done_reminder.py +7 -0
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_pr_done_reminder.py +21 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +4 -3
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapters.py +46 -4
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection_git.py +11 -0
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_selection.py +34 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/scripts/automatic_advisory/window_flags.py +0 -41
- package/scripts/repository_checks/config/email_exemptions.py +0 -10
package/bin/install.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
CURSOR_SYNC_SCRIPT_FILE_NAME,
|
|
35
35
|
CURSOR_RULES_DIRECTORY_NAME,
|
|
36
36
|
PSTACK_MODEL_RULE_FILE_NAME,
|
|
37
|
-
|
|
37
|
+
PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES,
|
|
38
38
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
39
39
|
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
40
40
|
} from './install-constants.mjs';
|
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
refreshPstackPluginManifest,
|
|
45
45
|
} from '../scripts/refresh_pstack_plugin_skills.mjs';
|
|
46
46
|
import { installPstack } from './pstack.mjs';
|
|
47
|
+
import {
|
|
48
|
+
configureContinuityHosts,
|
|
49
|
+
continuityHostConfigurationPaths,
|
|
50
|
+
removeContinuityHooks,
|
|
51
|
+
} from './install-session-continuity.mjs';
|
|
47
52
|
import {
|
|
48
53
|
resolveInstallRoot,
|
|
49
54
|
parseExplicitTargetFromArgv,
|
|
@@ -491,17 +496,10 @@ export function refreshInstalledPstackPluginManifest(
|
|
|
491
496
|
return join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
492
497
|
}
|
|
493
498
|
|
|
494
|
-
function
|
|
499
|
+
function seedPstackPreferences(preferenceFileName) {
|
|
495
500
|
const preferenceDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
496
|
-
const preferencePath = join(
|
|
497
|
-
|
|
498
|
-
PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
|
|
499
|
-
);
|
|
500
|
-
const preferenceSource = join(
|
|
501
|
-
PACKAGE_ROOT,
|
|
502
|
-
'bin',
|
|
503
|
-
PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
|
|
504
|
-
);
|
|
501
|
+
const preferencePath = join(preferenceDirectory, preferenceFileName);
|
|
502
|
+
const preferenceSource = join(PACKAGE_ROOT, 'bin', preferenceFileName);
|
|
505
503
|
mkdirSync(preferenceDirectory, { recursive: true });
|
|
506
504
|
try {
|
|
507
505
|
copyFileSync(
|
|
@@ -516,6 +514,63 @@ function seedCodexPstackPreferences() {
|
|
|
516
514
|
}
|
|
517
515
|
}
|
|
518
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Registers the session-continuity companion in each host configuration this
|
|
519
|
+
* install root resolves. Without this step the companion ships to the agents
|
|
520
|
+
* home but no host ever calls it, so Poteto Mode only activates when someone
|
|
521
|
+
* remembers a second setup command.
|
|
522
|
+
*
|
|
523
|
+
* A competing profile's registration, or an unwritable host configuration, is
|
|
524
|
+
* reported and leaves the rest of the install intact.
|
|
525
|
+
*
|
|
526
|
+
* @returns {string[]} The host configuration paths this run registered.
|
|
527
|
+
*/
|
|
528
|
+
function registerSessionContinuityHooks() {
|
|
529
|
+
const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
|
|
530
|
+
const hosts = Object.keys(configurationPaths)
|
|
531
|
+
.filter(host => existsSync(dirname(configurationPaths[host])));
|
|
532
|
+
if (hosts.length === 0) return [];
|
|
533
|
+
try {
|
|
534
|
+
const results = configureContinuityHosts(INSTALL_ROOT_RESOLUTION, hosts);
|
|
535
|
+
for (const result of results) {
|
|
536
|
+
const state = result.changed ? 'registered' : 'already registered';
|
|
537
|
+
console.log(` Session continuity: ${state} in ${result.path}`);
|
|
538
|
+
}
|
|
539
|
+
return results.map(result => result.path);
|
|
540
|
+
} catch (registrationError) {
|
|
541
|
+
console.warn(
|
|
542
|
+
` Warning: session-continuity hooks were not registered (${registrationError.message}) — `
|
|
543
|
+
+ 'run bin/install-session-continuity.mjs once the cause is resolved.',
|
|
544
|
+
);
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Removes the companion's registrations from the Codex and Cursor hook files
|
|
551
|
+
* during an uninstall. The Claude settings.json copy is pruned inside the
|
|
552
|
+
* settings block, which owns that file's single write.
|
|
553
|
+
*
|
|
554
|
+
* Each host this installer registered is a host it has to clean, or the host
|
|
555
|
+
* keeps calling a script the uninstall deleted.
|
|
556
|
+
*
|
|
557
|
+
* @returns {string[]} The host configuration paths this run changed.
|
|
558
|
+
*/
|
|
559
|
+
function removeSessionContinuityHooksFromOtherHosts() {
|
|
560
|
+
const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
|
|
561
|
+
const changedPaths = [];
|
|
562
|
+
for (const host of ['codex', 'cursor']) {
|
|
563
|
+
const configurationPath = configurationPaths[host];
|
|
564
|
+
if (!existsSync(configurationPath)) continue;
|
|
565
|
+
const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
|
|
566
|
+
if (removeContinuityHooks(configuration) === 0) continue;
|
|
567
|
+
writeFileSync(configurationPath, JSON.stringify(configuration, null, 2) + '\n');
|
|
568
|
+
changedPaths.push(configurationPath);
|
|
569
|
+
console.log(` Session continuity: hook registrations removed from ${configurationPath}`);
|
|
570
|
+
}
|
|
571
|
+
return changedPaths;
|
|
572
|
+
}
|
|
573
|
+
|
|
519
574
|
/**
|
|
520
575
|
* Formats an absolute interpreter path as a settings.json hook command prefix:
|
|
521
576
|
* forward-slash separators, double-quoted when the path contains a space so the
|
|
@@ -2756,10 +2811,12 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2756
2811
|
copyFileSync(pstackRuleSource, sharedRulePath);
|
|
2757
2812
|
allInstalledFiles.push(sharedRulePath);
|
|
2758
2813
|
syncWrittenPaths(allInstalledFiles);
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2814
|
+
for (const eachPreferenceFileName of PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES) {
|
|
2815
|
+
const seededPreferencePath = seedPstackPreferences(eachPreferenceFileName);
|
|
2816
|
+
if (seededPreferencePath) {
|
|
2817
|
+
allInstalledFiles.push(seededPreferencePath);
|
|
2818
|
+
allUserOwnedPreferencePaths.add(seededPreferencePath);
|
|
2819
|
+
}
|
|
2763
2820
|
}
|
|
2764
2821
|
syncWrittenPaths(allInstalledFiles);
|
|
2765
2822
|
}
|
|
@@ -2927,6 +2984,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2927
2984
|
console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
|
|
2928
2985
|
}
|
|
2929
2986
|
const isFullInstall = !selectedGroups;
|
|
2987
|
+
if (isFullInstall && shouldInstallAnyHooks) {
|
|
2988
|
+
summary.sessionContinuity = { configuredPaths: registerSessionContinuityHooks() };
|
|
2989
|
+
}
|
|
2930
2990
|
const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
|
|
2931
2991
|
let failedPrunePaths = [];
|
|
2932
2992
|
let stalePrunedTotal = 0;
|
|
@@ -3144,6 +3204,10 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3144
3204
|
pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
|
|
3145
3205
|
didSettingsChange = true;
|
|
3146
3206
|
console.log(' Hook entries removed from settings.json');
|
|
3207
|
+
const removedContinuityCount = removeContinuityHooks(settings);
|
|
3208
|
+
if (removedContinuityCount > 0) {
|
|
3209
|
+
console.log(` Session continuity: ${removedContinuityCount} hook registration(s) removed from settings.json`);
|
|
3210
|
+
}
|
|
3147
3211
|
}
|
|
3148
3212
|
const managedDenyFromPlan = plan.managedPermissionDenyEntries.length > 0
|
|
3149
3213
|
? plan.managedPermissionDenyEntries
|
|
@@ -3161,6 +3225,7 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3161
3225
|
writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
3162
3226
|
}
|
|
3163
3227
|
}
|
|
3228
|
+
removeSessionContinuityHooksFromOtherHosts();
|
|
3164
3229
|
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
3165
3230
|
|
|
3166
3231
|
unsetGlobalGitHooksPathIfOurs();
|
package/bin/install.test.mjs
CHANGED
|
@@ -3088,3 +3088,60 @@ test('--no-pstack leaves the pstack store absent', t => {
|
|
|
3088
3088
|
assert.equal(existsSync(join(homeDirectory, '.claude', 'pstack')), false);
|
|
3089
3089
|
});
|
|
3090
3090
|
});
|
|
3091
|
+
|
|
3092
|
+
function runContinuityInstaller(homeDirectory, extraArguments) {
|
|
3093
|
+
return execFileSync('node', [PSTACK_TEST_INSTALLER_PATH, ...extraArguments], {
|
|
3094
|
+
cwd: dirname(PSTACK_TEST_PACKAGE_ROOT),
|
|
3095
|
+
encoding: 'utf8',
|
|
3096
|
+
env: {
|
|
3097
|
+
...process.env,
|
|
3098
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3099
|
+
HOME: homeDirectory,
|
|
3100
|
+
USERPROFILE: homeDirectory,
|
|
3101
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3102
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3103
|
+
},
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
function continuityCommandCount(configurationPath) {
|
|
3108
|
+
if (!existsSync(configurationPath)) return 0;
|
|
3109
|
+
const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
|
|
3110
|
+
const allCommands = Object.values(configuration.hooks || {})
|
|
3111
|
+
.flat()
|
|
3112
|
+
.flatMap(entry => (entry.hooks ? entry.hooks.map(eachHook => eachHook.command) : [entry.command]));
|
|
3113
|
+
return allCommands.filter(
|
|
3114
|
+
eachCommand => typeof eachCommand === 'string'
|
|
3115
|
+
&& eachCommand.includes('session-continuity/continuity.mjs'),
|
|
3116
|
+
).length;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
test('a full install registers the session-continuity companion in every host, and uninstall takes it back out', t => {
|
|
3120
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-continuity-install-'));
|
|
3121
|
+
t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3122
|
+
const allConfigurationPaths = [
|
|
3123
|
+
join(homeDirectory, '.claude', 'settings.json'),
|
|
3124
|
+
join(homeDirectory, '.codex', 'hooks.json'),
|
|
3125
|
+
join(homeDirectory, '.cursor', 'hooks.json'),
|
|
3126
|
+
];
|
|
3127
|
+
|
|
3128
|
+
runContinuityInstaller(homeDirectory, []);
|
|
3129
|
+
|
|
3130
|
+
for (const configurationPath of allConfigurationPaths) {
|
|
3131
|
+
assert.equal(
|
|
3132
|
+
continuityCommandCount(configurationPath) > 0,
|
|
3133
|
+
true,
|
|
3134
|
+
`the install registers the companion in ${configurationPath} without a second setup command`,
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
runContinuityInstaller(homeDirectory, ['--uninstall']);
|
|
3139
|
+
|
|
3140
|
+
for (const configurationPath of allConfigurationPaths) {
|
|
3141
|
+
assert.equal(
|
|
3142
|
+
continuityCommandCount(configurationPath),
|
|
3143
|
+
0,
|
|
3144
|
+
`uninstall leaves no registration in ${configurationPath} pointing at a removed script`,
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"host": "claude",
|
|
3
|
+
"modelsByRole": {
|
|
4
|
+
"feature, refactoring": ["opus"],
|
|
5
|
+
"bug-fix": ["opus"],
|
|
6
|
+
"perf-issue": ["fable"],
|
|
7
|
+
"hillclimb": ["fable"],
|
|
8
|
+
"judgment and prose": ["fable"],
|
|
9
|
+
"hardest tasks": ["fable"],
|
|
10
|
+
"how explorer": ["sonnet"],
|
|
11
|
+
"how explainer": ["opus"],
|
|
12
|
+
"how critics": ["sonnet", "opus", "fable"],
|
|
13
|
+
"why investigators": ["sonnet"],
|
|
14
|
+
"why synthesizer": ["fable"],
|
|
15
|
+
"reflect tooling": ["opus"],
|
|
16
|
+
"reflect judgment, divergent, synthesizer": ["fable"],
|
|
17
|
+
"arena runners": ["sonnet", "opus", "fable"],
|
|
18
|
+
"arena cross-judge pool": ["opus", "fable"],
|
|
19
|
+
"swarm workers": ["sonnet"],
|
|
20
|
+
"architect runners": ["opus", "fable"],
|
|
21
|
+
"interrogate reviewers": ["sonnet", "opus", "fable"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -36,6 +36,12 @@ import subprocess
|
|
|
36
36
|
import sys
|
|
37
37
|
from pathlib import Path
|
|
38
38
|
|
|
39
|
+
_subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
|
|
40
|
+
if _subprocess_window_hooks_directory not in sys.path:
|
|
41
|
+
sys.path.append(_subprocess_window_hooks_directory)
|
|
42
|
+
|
|
43
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
44
|
+
|
|
39
45
|
try:
|
|
40
46
|
_hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
41
47
|
if _hooks_root_directory not in sys.path:
|
|
@@ -294,6 +300,7 @@ def _probe_pull_request(cwd: str) -> str | None:
|
|
|
294
300
|
errors="replace",
|
|
295
301
|
timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
296
302
|
check=False,
|
|
303
|
+
creationflags=hidden_window_creation_flags(),
|
|
297
304
|
)
|
|
298
305
|
except (OSError, subprocess.TimeoutExpired):
|
|
299
306
|
return None
|
|
@@ -11,6 +11,12 @@ import sys
|
|
|
11
11
|
from collections import Counter
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
+
_subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
|
|
15
|
+
if _subprocess_window_hooks_directory not in sys.path:
|
|
16
|
+
sys.path.append(_subprocess_window_hooks_directory)
|
|
17
|
+
|
|
18
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
19
|
+
|
|
14
20
|
try:
|
|
15
21
|
hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
16
22
|
if hooks_root_directory not in sys.path:
|
|
@@ -62,6 +68,7 @@ def _read_added_lines_from_git(all_git_arguments: tuple[str, ...], file_path: st
|
|
|
62
68
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
63
69
|
cwd=working_directory,
|
|
64
70
|
env=_git_environment(),
|
|
71
|
+
creationflags=hidden_window_creation_flags(),
|
|
65
72
|
)
|
|
66
73
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
67
74
|
return []
|
|
@@ -106,6 +113,7 @@ def is_new_file(file_path: str) -> bool:
|
|
|
106
113
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
107
114
|
cwd=working_directory,
|
|
108
115
|
env=_git_environment(),
|
|
116
|
+
creationflags=hidden_window_creation_flags(),
|
|
109
117
|
)
|
|
110
118
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
111
119
|
return False
|
|
@@ -28,6 +28,10 @@ except ImportError as import_error:
|
|
|
28
28
|
) from import_error
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
WINDOWS_NO_WINDOW_FLAG = 0x08000000
|
|
32
|
+
EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
|
|
33
|
+
|
|
34
|
+
|
|
31
35
|
_CLEAN_PR_OBJECT: dict[str, object] = {
|
|
32
36
|
"number": 42,
|
|
33
37
|
"url": "https://github.com/acme/widgets/pull/42",
|
|
@@ -248,3 +252,20 @@ def test_main_should_stay_quiet_on_any_other_gh_failure(
|
|
|
248
252
|
|
|
249
253
|
assert _run_main(monkeypatch, capsys, _payload("git push"), gh_down) == ""
|
|
250
254
|
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_gh_probe_should_start_without_a_console_window(
|
|
258
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
259
|
+
) -> None:
|
|
260
|
+
all_recorded_keyword_arguments: dict[str, object] = {}
|
|
261
|
+
|
|
262
|
+
def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
263
|
+
del args
|
|
264
|
+
all_recorded_keyword_arguments.update(kwargs)
|
|
265
|
+
return subprocess.CompletedProcess(
|
|
266
|
+
[], 0, stdout=json.dumps(_CLEAN_PR_OBJECT), stderr=""
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
_run_main(monkeypatch, capsys, _payload("git push"), _record)
|
|
270
|
+
|
|
271
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
@@ -9,9 +9,12 @@ return no added lines, so a qualifying rename is eligible without fixture setup.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
15
18
|
try:
|
|
16
19
|
_ADVISORY_DIRECTORY = Path(__file__).resolve().parent
|
|
17
20
|
if str(_ADVISORY_DIRECTORY) not in sys.path:
|
|
@@ -25,6 +28,10 @@ except ImportError as import_error:
|
|
|
25
28
|
) from import_error
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
WINDOWS_NO_WINDOW_FLAG = 0x08000000
|
|
32
|
+
EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
|
|
33
|
+
|
|
34
|
+
|
|
28
35
|
def _multi_edit_payload(file_path: str, all_edits: list[dict[str, str]]) -> dict[str, object]:
|
|
29
36
|
return {
|
|
30
37
|
"tool_name": "MultiEdit",
|
|
@@ -85,3 +92,36 @@ def test_multi_edit_returns_none_when_no_edit_qualifies(tmp_path: Path) -> None:
|
|
|
85
92
|
)
|
|
86
93
|
|
|
87
94
|
assert refactor_guard._multi_edit_refactor_advisory_description(payload) is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _record_creation_flags(
|
|
98
|
+
monkeypatch: pytest.MonkeyPatch, standard_output: str
|
|
99
|
+
) -> dict[str, object]:
|
|
100
|
+
all_recorded_keyword_arguments: dict[str, object] = {}
|
|
101
|
+
|
|
102
|
+
def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
103
|
+
del args
|
|
104
|
+
all_recorded_keyword_arguments.update(kwargs)
|
|
105
|
+
return subprocess.CompletedProcess([], 0, stdout=standard_output, stderr="")
|
|
106
|
+
|
|
107
|
+
monkeypatch.setattr(refactor_guard.subprocess, "run", _record)
|
|
108
|
+
return all_recorded_keyword_arguments
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_git_diff_should_start_without_a_console_window(
|
|
112
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
113
|
+
) -> None:
|
|
114
|
+
all_recorded_keyword_arguments = _record_creation_flags(monkeypatch, "+added line\n")
|
|
115
|
+
|
|
116
|
+
refactor_guard.get_git_diff_added_lines(str(tmp_path / "module.py"))
|
|
117
|
+
|
|
118
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_untracked_probe_should_start_without_a_console_window(
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
123
|
+
) -> None:
|
|
124
|
+
all_recorded_keyword_arguments = _record_creation_flags(monkeypatch, "module.py\n")
|
|
125
|
+
|
|
126
|
+
assert refactor_guard.is_new_file(str(tmp_path / "module.py")) is True
|
|
127
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
@@ -23,6 +23,8 @@ if _hooks_dir not in sys.path:
|
|
|
23
23
|
|
|
24
24
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
25
25
|
|
|
26
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
27
|
+
|
|
26
28
|
GIT_COMMAND_TIMEOUT_SECONDS = 5
|
|
27
29
|
PROTECTED_BRANCHES = ("main", "master")
|
|
28
30
|
PROTECTED_REMOTE_PATTERNS: list[str] = []
|
|
@@ -111,6 +113,7 @@ def get_branch_at_directory(working_dir: str | None = None) -> str | None:
|
|
|
111
113
|
text=True,
|
|
112
114
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
113
115
|
cwd=working_dir,
|
|
116
|
+
creationflags=hidden_window_creation_flags(),
|
|
114
117
|
)
|
|
115
118
|
if completed_process.returncode == 0:
|
|
116
119
|
return completed_process.stdout.strip()
|
|
@@ -130,6 +133,7 @@ def is_protected_repo(working_dir: str | None = None) -> bool:
|
|
|
130
133
|
text=True,
|
|
131
134
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
132
135
|
cwd=working_dir,
|
|
136
|
+
creationflags=hidden_window_creation_flags(),
|
|
133
137
|
)
|
|
134
138
|
if completed_process.returncode == 0:
|
|
135
139
|
remote_url = completed_process.stdout.strip()
|
|
@@ -30,6 +30,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
30
30
|
ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES,
|
|
31
31
|
CHAINED_INLINE_COMMENT_PATTERN,
|
|
32
32
|
MAX_COMMENT_ISSUES,
|
|
33
|
+
STEALTH_KEEP_COMMENT_MARKER,
|
|
33
34
|
)
|
|
34
35
|
_javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
|
|
35
36
|
extract_javascript_comment_occurrences = (
|
|
@@ -152,10 +153,36 @@ def _python_comment_occurrences(
|
|
|
152
153
|
return all_occurrences, True
|
|
153
154
|
|
|
154
155
|
|
|
156
|
+
def _is_stealth_keep_occurrence(comment_text: str) -> bool:
|
|
157
|
+
"""Return True for a ``# STEALTH: Keep`` occurrence.
|
|
158
|
+
|
|
159
|
+
``STEALTH: Keep`` is meant to be freely added and to survive edits to
|
|
160
|
+
the line it sits on, unlike ``noqa`` or ``TODO``, which AGENTS.md
|
|
161
|
+
requires to be removed rather than added or justified. It is excluded
|
|
162
|
+
here rather than through the shared exempt-marker set that
|
|
163
|
+
``check_comment_changes`` deliberately still blocks on add.
|
|
164
|
+
"""
|
|
165
|
+
return comment_text.startswith("#") and comment_text[1:].lstrip().startswith(
|
|
166
|
+
STEALTH_KEEP_COMMENT_MARKER
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _without_stealth_keep_occurrences(
|
|
171
|
+
all_occurrences: list[tuple[str, int, bool]]
|
|
172
|
+
) -> list[tuple[str, int, bool]]:
|
|
173
|
+
return [
|
|
174
|
+
each_occurrence
|
|
175
|
+
for each_occurrence in all_occurrences
|
|
176
|
+
if not _is_stealth_keep_occurrence(each_occurrence[0])
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
155
180
|
def check_comment_changes(old_content: str, new_content: str, file_path: str) -> list[str]:
|
|
156
181
|
"""Check for comment additions or removals between old and new content.
|
|
157
182
|
|
|
158
|
-
Inline and standalone comments are blocking findings when added
|
|
183
|
+
Inline and standalone comments are blocking findings when added, except
|
|
184
|
+
``STEALTH: Keep`` occurrences, which are excluded entirely so the marker
|
|
185
|
+
can be added and can persist across edits to its line.
|
|
159
186
|
Existing comments can be removed when the touched code no longer needs them.
|
|
160
187
|
|
|
161
188
|
When the file is Python and either *old_content* or *new_content* cannot
|
|
@@ -178,6 +205,9 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
|
|
|
178
205
|
old_occurrences = extract_javascript_comment_occurrences(old_content, True)
|
|
179
206
|
new_occurrences = extract_javascript_comment_occurrences(new_content, True)
|
|
180
207
|
|
|
208
|
+
old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
|
|
209
|
+
new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
|
|
210
|
+
|
|
181
211
|
old_occurrence_counts = Counter(
|
|
182
212
|
(each_text, is_inline)
|
|
183
213
|
for each_text, _line_number, is_inline in old_occurrences
|
|
@@ -264,6 +294,8 @@ def _retained_comment_issues(
|
|
|
264
294
|
(old_occurrences, old_tokenize_ok), (new_occurrences, new_tokenize_ok) = (_comment_occurrences(old_content, file_path, True), _comment_occurrences(new_content, file_path, True))
|
|
265
295
|
if not (old_tokenize_ok and new_tokenize_ok):
|
|
266
296
|
return []
|
|
297
|
+
old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
|
|
298
|
+
new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
|
|
267
299
|
all_changed_lines, all_deleted_lines, old_line_by_new_line = _line_diff_data(old_content, new_content)
|
|
268
300
|
old_line_by_key = {
|
|
269
301
|
each_key: [each_line for each_text, each_line, each_is_inline in old_occurrences if (each_text, each_is_inline) == each_key]
|
|
@@ -346,12 +378,14 @@ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
|
|
|
346
378
|
token-anchored directive body never legitimately carries a ``#``
|
|
347
379
|
(noqa codes, pylint symbols, and pragma directives contain none), so
|
|
348
380
|
any inner ``#`` reliably marks chained prose. Free-form markers
|
|
349
|
-
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``)
|
|
350
|
-
trailing prose:
|
|
381
|
+
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``, ``STEALTH: Keep``)
|
|
382
|
+
accept any trailing prose:
|
|
351
383
|
``# type:`` participates in the documented justification
|
|
352
384
|
convention enforced by ``check_type_escape_hatches`` (which
|
|
353
|
-
requires a trailing reason),
|
|
354
|
-
annotation text by convention
|
|
385
|
+
requires a trailing reason), the TODO-family markers carry
|
|
386
|
+
annotation text by convention, and ``STEALTH: Keep`` is the
|
|
387
|
+
acknowledged-bypass marker the CDP runtime-usage scanner reads
|
|
388
|
+
from the flagged line together with its reason.
|
|
355
389
|
"""
|
|
356
390
|
comment_string = comment_token.string
|
|
357
391
|
if comment_string.startswith("#!") and comment_token.start == (1, 0):
|
|
@@ -16,6 +16,8 @@ import sys
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from urllib.parse import urlparse
|
|
18
18
|
|
|
19
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
20
|
+
|
|
19
21
|
try:
|
|
20
22
|
_blocking_directory = str(Path(__file__).resolve().parent.parent)
|
|
21
23
|
_hooks_directory = str(Path(__file__).resolve().parent.parent.parent)
|
|
@@ -186,6 +188,7 @@ def _repository_origin_slug(repository_root: Path) -> str | None:
|
|
|
186
188
|
text=True,
|
|
187
189
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
188
190
|
cwd=str(repository_root),
|
|
191
|
+
creationflags=hidden_window_creation_flags(),
|
|
189
192
|
)
|
|
190
193
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
191
194
|
return None
|
|
@@ -18,6 +18,8 @@ from hooks_constants.precommit_code_rules_gate_constants import ( # noqa: E402
|
|
|
18
18
|
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def resolve_repository_root(working_directory: str | None) -> Path | None:
|
|
23
25
|
"""Resolve the Git repository root for a directory.
|
|
@@ -36,6 +38,7 @@ def resolve_repository_root(working_directory: str | None) -> Path | None:
|
|
|
36
38
|
text=True,
|
|
37
39
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
38
40
|
cwd=working_directory,
|
|
41
|
+
creationflags=hidden_window_creation_flags(),
|
|
39
42
|
)
|
|
40
43
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
41
44
|
return None
|
|
@@ -30,6 +30,10 @@ def _safe_constant_attribute_calls() -> frozenset[tuple[str, str]]:
|
|
|
30
30
|
return frozenset({("re", "compile")})
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _safe_string_literal_methods() -> frozenset[str]:
|
|
34
|
+
return frozenset({"join"})
|
|
35
|
+
|
|
36
|
+
|
|
33
37
|
def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
|
|
34
38
|
return isinstance(
|
|
35
39
|
subnode,
|
|
@@ -37,7 +41,18 @@ def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
|
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
|
|
44
|
+
def _is_safe_string_literal_method_call(function_node: ast.Attribute) -> bool:
|
|
45
|
+
receiver = function_node.value
|
|
46
|
+
return (
|
|
47
|
+
isinstance(receiver, ast.Constant)
|
|
48
|
+
and isinstance(receiver.value, str)
|
|
49
|
+
and function_node.attr in _safe_string_literal_methods()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
40
53
|
def _attribute_call_is_unsafe(function_node: ast.Attribute) -> bool:
|
|
54
|
+
if _is_safe_string_literal_method_call(function_node):
|
|
55
|
+
return False
|
|
41
56
|
if not isinstance(function_node.value, ast.Name):
|
|
42
57
|
return True
|
|
43
58
|
pair = (function_node.value.id, function_node.attr)
|
|
@@ -56,9 +71,10 @@ def _call_is_unsafe(call_node: ast.Call) -> bool:
|
|
|
56
71
|
def _rhs_has_unsafe_call(rhs_node: ast.AST) -> bool:
|
|
57
72
|
"""Return True when a constant's value contains a non-allowlisted call.
|
|
58
73
|
|
|
59
|
-
Safe calls are value constructors (``Path(...)``, ``re.compile(...)``)
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
Safe calls are value constructors (``Path(...)``, ``re.compile(...)``) and
|
|
75
|
+
a pure string-literal method call (``"|".join(...)``) that build objects
|
|
76
|
+
without side effects; any other call or a comprehension reads as
|
|
77
|
+
import-time behavior.
|
|
62
78
|
|
|
63
79
|
Args:
|
|
64
80
|
rhs_node: The assignment's right-hand-side expression node.
|
|
@@ -29,6 +29,8 @@ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
|
|
|
29
29
|
GIT_PATHSPEC_SEPARATOR,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
def _git_ls_files_command(file_name: str) -> list[str]:
|
|
34
36
|
return [GIT_EXECUTABLE_NAME, GIT_LS_FILES_SUBCOMMAND, GIT_PATHSPEC_SEPARATOR, file_name]
|
|
@@ -57,6 +59,7 @@ def _git_tracks_path(path: Path) -> bool:
|
|
|
57
59
|
text=True,
|
|
58
60
|
check=False,
|
|
59
61
|
timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
|
|
62
|
+
creationflags=hidden_window_creation_flags(),
|
|
60
63
|
)
|
|
61
64
|
except (OSError, subprocess.SubprocessError):
|
|
62
65
|
return False
|
|
@@ -80,6 +83,7 @@ def _working_tree_differs_from_head(path: Path) -> bool:
|
|
|
80
83
|
text=True,
|
|
81
84
|
check=False,
|
|
82
85
|
timeout=GIT_DIFF_TIMEOUT_SECONDS,
|
|
86
|
+
creationflags=hidden_window_creation_flags(),
|
|
83
87
|
)
|
|
84
88
|
except (OSError, subprocess.SubprocessError):
|
|
85
89
|
return True
|
|
@@ -12,6 +12,31 @@ def test_constants_only_rejects_function_definition() -> None:
|
|
|
12
12
|
assert content_analysis._is_constants_only_python_content("def f():\n return 1\n") is False
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def test_constants_only_accepts_join_on_a_string_literal_inside_re_compile() -> None:
|
|
16
|
+
content = (
|
|
17
|
+
'"""doc."""\n'
|
|
18
|
+
"import re\n"
|
|
19
|
+
"NAMES = ('one', 'two')\n"
|
|
20
|
+
'PATTERN = re.compile("(" + "|".join(NAMES) + ")")\n'
|
|
21
|
+
)
|
|
22
|
+
assert content_analysis._is_constants_only_python_content(content) is True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_constants_only_rejects_join_on_a_non_literal_receiver() -> None:
|
|
26
|
+
content = (
|
|
27
|
+
'"""doc."""\n'
|
|
28
|
+
'SEPARATOR = "|"\n'
|
|
29
|
+
'NAMES = ("one", "two")\n'
|
|
30
|
+
"PATTERN = SEPARATOR.join(NAMES)\n"
|
|
31
|
+
)
|
|
32
|
+
assert content_analysis._is_constants_only_python_content(content) is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_constants_only_rejects_a_non_allowlisted_method_on_a_string_literal() -> None:
|
|
36
|
+
content = '"""doc."""\nSHOUT = "hi".upper()\n'
|
|
37
|
+
assert content_analysis._is_constants_only_python_content(content) is False
|
|
38
|
+
|
|
39
|
+
|
|
15
40
|
def test_post_edit_import_only_allows_import_removal() -> None:
|
|
16
41
|
existing = "import os\n\ndef run(): return 1\n"
|
|
17
42
|
tool_input = {"old_string": "import os\n", "new_string": ""}
|
|
@@ -133,6 +133,33 @@ def test_check_comment_changes_allows_comment_removal_without_advisory(
|
|
|
133
133
|
assert captured.err == ""
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def test_check_comment_changes_does_not_flag_added_stealth_keep_marker() -> None:
|
|
137
|
+
old_content = "x = 1\n"
|
|
138
|
+
new_content = "x = 1 # STEALTH: Keep -- reason\n"
|
|
139
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
140
|
+
assert issues == []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_check_comment_changes_does_not_flag_stealth_keep_retained_on_touched_line() -> None:
|
|
144
|
+
old_content = "x = 1 # STEALTH: Keep -- reason\n"
|
|
145
|
+
new_content = "x = 2 # STEALTH: Keep -- reason\n"
|
|
146
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
147
|
+
assert issues == []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
"each_marker",
|
|
152
|
+
("# noqa: E501", "# type: ignore[misc]", "# TODO: revisit", "# FIXME: revisit"),
|
|
153
|
+
)
|
|
154
|
+
def test_check_comment_changes_still_flags_other_added_directive_markers(
|
|
155
|
+
each_marker: str,
|
|
156
|
+
) -> None:
|
|
157
|
+
old_content = "x = 1\n"
|
|
158
|
+
new_content = f"x = 1 {each_marker}\n"
|
|
159
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
160
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
161
|
+
|
|
162
|
+
|
|
136
163
|
def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
|
|
137
164
|
for each_directive in ("#noqa", "#type: ignore", "#pylint: disable", "#pragma: no cover"):
|
|
138
165
|
content = f"{each_directive}\n"
|