@devrik-tools/claude-gates 0.2.0 → 0.3.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/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +7 -6
- package/README.md +16 -7
- package/cli/index.mjs +4 -0
- package/cli/init.mjs +18 -1
- package/cli/install.mjs +26 -1
- package/package.json +5 -2
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -6
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +78 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +11 -81
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +140 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +130 -126
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +104 -79
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +138 -123
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +59 -44
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +124 -101
- package/plugins/gates/hooks/hooks.json +365 -346
- package/plugins/gates/hooks/lib/hook-io.mjs +39 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/register-requests.mjs +7 -3
- package/registry.json +738 -793
- package/cli/__tests__/config.test.mjs +0 -113
- package/cli/__tests__/init-flags.test.mjs +0 -111
- package/cli/__tests__/install.test.mjs +0 -102
- package/cli/__tests__/materialize.test.mjs +0 -95
- package/cli/__tests__/registry-gates-consistency.test.mjs +0 -52
- package/cli/__tests__/selection.test.mjs +0 -200
- package/cli/__tests__/task.test.mjs +0 -124
- package/cli/__tests__/version-consistency.test.mjs +0 -44
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +0 -83
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +0 -85
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +0 -65
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +0 -117
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +0 -104
- package/plugins/gates/hooks/gates/autonomous-mode/test.mjs +0 -58
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +0 -165
- package/plugins/gates/hooks/gates/bash-commands/test.mjs +0 -126
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +0 -151
- package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +0 -111
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +0 -207
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +0 -161
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +0 -69
- package/plugins/gates/hooks/gates/dependency-skills/test.mjs +0 -109
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +0 -68
- package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +0 -68
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/feature-catalog/test.mjs +0 -97
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +0 -88
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +0 -155
- package/plugins/gates/hooks/gates/forge-flow/test.mjs +0 -135
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +0 -66
- package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +0 -86
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +0 -96
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +0 -165
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +0 -85
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +0 -100
- package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +0 -119
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +0 -80
- package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +0 -65
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +0 -71
- package/plugins/gates/hooks/gates/never-assume/test.mjs +0 -95
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +0 -109
- package/plugins/gates/hooks/gates/no-blocking/test.mjs +0 -108
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +0 -106
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +0 -91
- package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +0 -131
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/protected-paths/test.mjs +0 -75
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +0 -102
- package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +0 -99
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +0 -94
- package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +0 -101
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +0 -116
- package/plugins/gates/hooks/gates/risk-level/test.mjs +0 -153
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +0 -58
- package/plugins/gates/hooks/gates/root-cause-first/test.mjs +0 -73
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +0 -97
- package/plugins/gates/hooks/gates/root-whitelist/test.mjs +0 -80
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +0 -78
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +0 -130
- package/plugins/gates/hooks/gates/sdd-specs/test.mjs +0 -163
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +0 -114
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +0 -72
- package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +0 -81
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +0 -63
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +0 -114
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +0 -62
- package/plugins/gates/hooks/gates/tool-map/test.mjs +0 -87
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +0 -173
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +0 -210
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +0 -114
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +0 -189
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +0 -100
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +0 -95
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +0 -180
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Edge-case probes for mandatory-flow. Previously: (1) the prompt was read only from
|
|
2
|
-
// prompt/Prompt/task, so a Task delegation carrying its brief in `description`
|
|
3
|
-
// bypassed the check (shared root cause with implementation-pipeline, test-matrix,
|
|
4
|
-
// sdd-specs delegation check); (2) the ACTIVA pointer's slug was used unsanitized in
|
|
5
|
-
// join(cwd, '.ai', 'pipeline', slug), so a slug with '..' segments let
|
|
6
|
-
// hasTaskContract() resolve outside .ai/pipeline/ and accept an unrelated file
|
|
7
|
-
// anywhere on disk as the active task's contract. Both are now fixed:
|
|
8
|
-
// delegationPromptOf() reads description too, and readSlug() rejects any slug
|
|
9
|
-
// containing a path separator or a '.'/'..' segment, treating it as empty (which
|
|
10
|
-
// denies the same way a missing slug does).
|
|
11
|
-
import assert from 'node:assert/strict';
|
|
12
|
-
import { execFileSync } from 'node:child_process';
|
|
13
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { tmpdir } from 'node:os';
|
|
15
|
-
import { dirname, join } from 'node:path';
|
|
16
|
-
import { test } from 'node:test';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
|
|
19
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
20
|
-
|
|
21
|
-
function makeProject() {
|
|
22
|
-
const project = mkdtempSync(join(tmpdir(), 'mandatory-flow-edge-'));
|
|
23
|
-
mkdirSync(join(project, '.git'));
|
|
24
|
-
return project;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function writeProjectConfig(project, config) {
|
|
28
|
-
mkdirSync(join(project, '.ai'), { recursive: true });
|
|
29
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function runGate(project, payload) {
|
|
33
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
34
|
-
input: JSON.stringify(payload),
|
|
35
|
-
encoding: 'utf8',
|
|
36
|
-
cwd: project,
|
|
37
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
38
|
-
});
|
|
39
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isDeny(result) {
|
|
43
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function enableGate(project) {
|
|
47
|
-
writeProjectConfig(project, { gates: { requireLiveTaskWhenImplementing: true } });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
test('FIXED: a Task delegation carrying its brief in `description` is now checked', () => {
|
|
51
|
-
const project = makeProject();
|
|
52
|
-
enableGate(project);
|
|
53
|
-
// No ACTIVA pointer at all -- with delegationPromptOf reading `description`, this
|
|
54
|
-
// now reaches checkLiveTask and denies for the missing pointer.
|
|
55
|
-
const payload = {
|
|
56
|
-
tool_name: 'Task',
|
|
57
|
-
tool_input: {
|
|
58
|
-
description: 'Nivel: STANDARD\nImplementá el checkout.',
|
|
59
|
-
subagent_type: 'backend',
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
assert.ok(
|
|
63
|
-
isDeny(runGate(project, payload)),
|
|
64
|
-
'delegationPromptOf reads description and the no-live-task deny fires',
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('OK: the equivalent payload using `prompt` with no ACTIVA pointer IS denied (control)', () => {
|
|
69
|
-
const project = makeProject();
|
|
70
|
-
enableGate(project);
|
|
71
|
-
const payload = {
|
|
72
|
-
tool_name: 'Task',
|
|
73
|
-
tool_input: {
|
|
74
|
-
prompt: 'Nivel: STANDARD\nImplementá el checkout.',
|
|
75
|
-
subagent_type: 'backend',
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
assert.ok(isDeny(runGate(project, payload)), 'control failed');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('FIXED: a path-traversal slug in ACTIVA no longer lets an unrelated file satisfy hasTaskContract', () => {
|
|
82
|
-
const project = makeProject();
|
|
83
|
-
enableGate(project);
|
|
84
|
-
mkdirSync(join(project, '.ai', 'pipeline'), { recursive: true });
|
|
85
|
-
mkdirSync(join(project, 'evil'), { recursive: true });
|
|
86
|
-
writeFileSync(join(project, 'evil', 'asserts.md'), 'unrelated content');
|
|
87
|
-
writeFileSync(join(project, '.ai', 'pipeline', 'ACTIVA'), '../../evil');
|
|
88
|
-
const payload = {
|
|
89
|
-
tool_name: 'Agent',
|
|
90
|
-
tool_input: {
|
|
91
|
-
prompt: 'Nivel: STANDARD\nImplementá el checkout.',
|
|
92
|
-
subagent_type: 'backend',
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
assert.ok(
|
|
96
|
-
isDeny(runGate(project, payload)),
|
|
97
|
-
'readSlug must reject the traversal slug (treated as empty), denying instead of ' +
|
|
98
|
-
'accepting the unrelated file outside .ai/pipeline/',
|
|
99
|
-
);
|
|
100
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function makeProject() {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'mandatory-flow-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
return project;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function writeProjectConfig(project, config) {
|
|
18
|
-
mkdirSync(join(project, '.ai'), { recursive: true });
|
|
19
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function runGate(project, payload) {
|
|
23
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
24
|
-
input: JSON.stringify(payload),
|
|
25
|
-
encoding: 'utf8',
|
|
26
|
-
cwd: project,
|
|
27
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
28
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
29
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
30
|
-
});
|
|
31
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function delegate(prompt, subagentType = 'backend') {
|
|
35
|
-
return {
|
|
36
|
-
tool_name: 'Agent',
|
|
37
|
-
tool_input: { prompt, subagent_type: subagentType },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isDeny(result) {
|
|
42
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function enableGate(project) {
|
|
46
|
-
writeProjectConfig(project, {
|
|
47
|
-
gates: { requireLiveTaskWhenImplementing: true },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
test('disabled by default: no pointer file needed', () => {
|
|
52
|
-
const project = makeProject();
|
|
53
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
54
|
-
assert.equal(runGate(project, delegate(prompt)), null);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('denies implementing when no active-task pointer exists on disk', () => {
|
|
58
|
-
const project = makeProject();
|
|
59
|
-
enableGate(project);
|
|
60
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
61
|
-
assert.ok(isDeny(runGate(project, delegate(prompt))));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('allows implementing when the active task has a contract on disk', () => {
|
|
65
|
-
const project = makeProject();
|
|
66
|
-
enableGate(project);
|
|
67
|
-
mkdirSync(join(project, '.ai', 'pipeline', 'checkout-flow'), {
|
|
68
|
-
recursive: true,
|
|
69
|
-
});
|
|
70
|
-
writeFileSync(join(project, '.ai', 'pipeline', 'ACTIVA'), 'checkout-flow');
|
|
71
|
-
writeFileSync(
|
|
72
|
-
join(project, '.ai', 'pipeline', 'checkout-flow', 'asserts.md'),
|
|
73
|
-
'# Asserts\n\nCriterio: cobrar bien.',
|
|
74
|
-
);
|
|
75
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
76
|
-
assert.equal(runGate(project, delegate(prompt)), null);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('denies when the pointer names a task with no contract file', () => {
|
|
80
|
-
const project = makeProject();
|
|
81
|
-
enableGate(project);
|
|
82
|
-
mkdirSync(join(project, '.ai', 'pipeline', 'checkout-flow'), {
|
|
83
|
-
recursive: true,
|
|
84
|
-
});
|
|
85
|
-
writeFileSync(join(project, '.ai', 'pipeline', 'ACTIVA'), 'checkout-flow');
|
|
86
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
87
|
-
assert.ok(isDeny(runGate(project, delegate(prompt))));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('exempt subagent type (qa) is never held to the live-task requirement', () => {
|
|
91
|
-
const project = makeProject();
|
|
92
|
-
enableGate(project);
|
|
93
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
94
|
-
assert.equal(runGate(project, delegate(prompt, 'qa')), null);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('activePointerPath param override points the gate at a custom location', () => {
|
|
98
|
-
const project = makeProject();
|
|
99
|
-
writeProjectConfig(project, {
|
|
100
|
-
gates: {
|
|
101
|
-
requireLiveTaskWhenImplementing: {
|
|
102
|
-
enabled: true,
|
|
103
|
-
activePointerPath: join('.custom', 'ACTIVE'),
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
mkdirSync(join(project, '.ai', 'pipeline', 'checkout-flow'), {
|
|
108
|
-
recursive: true,
|
|
109
|
-
});
|
|
110
|
-
writeFileSync(join(project, '.ai', 'pipeline', 'ACTIVA'), 'checkout-flow');
|
|
111
|
-
writeFileSync(
|
|
112
|
-
join(project, '.ai', 'pipeline', 'checkout-flow', 'asserts.md'),
|
|
113
|
-
'Criterio: cobrar bien.',
|
|
114
|
-
);
|
|
115
|
-
const prompt = 'Nivel: STANDARD\nImplementá el checkout.';
|
|
116
|
-
// the default pointer exists and is well-formed, but the override points elsewhere,
|
|
117
|
-
// which does not exist: the gate must deny, proving the param actually took effect.
|
|
118
|
-
assert.ok(isDeny(runGate(project, delegate(prompt))));
|
|
119
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'neutral-spanish-edge-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
23
|
-
});
|
|
24
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isWarn(result) {
|
|
28
|
-
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// FIXED: extractText() now gates on toolInGroups(toolName, ['write']) and reads through
|
|
32
|
-
// writtenContentOf() (lib/hook-io.mjs), which covers NotebookEdit's new_source field.
|
|
33
|
-
test('FIXED: NotebookEdit carrying regional Spanish is detected', () => {
|
|
34
|
-
const payload = {
|
|
35
|
-
tool_name: 'NotebookEdit',
|
|
36
|
-
tool_input: {
|
|
37
|
-
notebook_path: '/repo/notebook.ipynb',
|
|
38
|
-
cell_id: 'abc',
|
|
39
|
-
new_source: 'vos tenés que revisar esto, dale',
|
|
40
|
-
cell_type: 'markdown',
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
assert.ok(isWarn(runGate(payload)), 'gate now detects regional Spanish written via NotebookEdit');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// FIXED: write_to_file is recognized by toolInGroups(toolName, ['write']) (native name in
|
|
47
|
-
// TOOL_GROUPS.write) and its `content` field is read through writtenContentOf().
|
|
48
|
-
test('FIXED: write_to_file with regional Spanish content is detected', () => {
|
|
49
|
-
const payload = {
|
|
50
|
-
tool_name: 'write_to_file',
|
|
51
|
-
tool_input: {
|
|
52
|
-
file_path: '/repo/notes.md',
|
|
53
|
-
content: 'che, mirá el laburo que hicimos acá',
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
assert.ok(isWarn(runGate(payload)), 'gate now detects regional Spanish written via write_to_file');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// EDGE CASE 3: false-positive check on legitimate neutral words that are substrings-adjacent
|
|
60
|
-
// to markers. "sos" (voseo for "you are") is in the marker list; verify a word that merely
|
|
61
|
-
// CONTAINS "sos" as a substring (e.g. "sospecha", "hermosos") is NOT flagged, since the
|
|
62
|
-
// pattern uses \p{L} boundaries.
|
|
63
|
-
test('OK: word boundary prevents false positive on words containing a marker as substring', () => {
|
|
64
|
-
const result = runGate({
|
|
65
|
-
tool_name: 'Write',
|
|
66
|
-
tool_input: { file_path: '/repo/notes.md', content: 'Tengo una sospecha sobre estos hermosos resultados.' },
|
|
67
|
-
});
|
|
68
|
-
assert.equal(result, null, 'false positive: "sos" matched inside sospecha/hermosos');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// EDGE CASE 4: "dale" is in the marker list as Rioplatense slang, but is this over-broad?
|
|
72
|
-
// "dale" only appears as that exact word — check it correctly fires only for that token,
|
|
73
|
-
// not, e.g., as part of "dalear" or similar. This confirms no over-matching regression.
|
|
74
|
-
test('OK: marker "dale" does not fire on a word merely containing it', () => {
|
|
75
|
-
const result = runGate({
|
|
76
|
-
tool_name: 'Write',
|
|
77
|
-
tool_input: { file_path: '/repo/notes.md', content: 'Actualiza el dataleer o el dalext.' },
|
|
78
|
-
});
|
|
79
|
-
assert.equal(result, null, 'false positive: "dale" matched inside a longer word');
|
|
80
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'neutral-spanish-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
-
});
|
|
26
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function write(content) {
|
|
30
|
-
return {
|
|
31
|
-
tool_name: 'Write',
|
|
32
|
-
tool_input: { file_path: '/repo/notes.md', content },
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function isWarn(result) {
|
|
36
|
-
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
test('warns on Rioplatense voseo and lexicon', () => {
|
|
40
|
-
assert.ok(isWarn(runGate(write('vos tenés que revisar esto, dale'))));
|
|
41
|
-
assert.ok(isWarn(runGate(write('che, mirá el laburo que hicimos acá'))));
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('allows neutral Spanish', () => {
|
|
45
|
-
assert.equal(runGate(write('tienes que revisar esto, de acuerdo')), null);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('disabled by config: the gate does not run', () => {
|
|
49
|
-
assert.equal(
|
|
50
|
-
runGate(write('vos tenés que revisar esto'), {
|
|
51
|
-
config: { gates: { warnNonNeutralSpanish: false } },
|
|
52
|
-
}),
|
|
53
|
-
null,
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('project regionalMarkers override replaces the built-in list', () => {
|
|
58
|
-
const config = {
|
|
59
|
-
gates: {
|
|
60
|
-
warnNonNeutralSpanish: { enabled: true, regionalMarkers: ['bacano'] },
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
assert.equal(runGate(write('vos tenés que revisar esto'), { config }), null);
|
|
64
|
-
assert.ok(isWarn(runGate(write('que bacano quedo esto'), { config })));
|
|
65
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'never-assume-edge-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
23
|
-
});
|
|
24
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isWarn(result) {
|
|
28
|
-
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const ENABLE = { config: { gates: { requireVerificationBeforeAssuming: true } } };
|
|
32
|
-
|
|
33
|
-
// FIXED: extractContent() now reads through writtenContentOf() (lib/hook-io.mjs), which
|
|
34
|
-
// covers NotebookEdit's new_source field, closing the tool-coverage gap this test used to
|
|
35
|
-
// document as a bug.
|
|
36
|
-
test('FIXED: conjecture phrasing written via NotebookEdit is detected', () => {
|
|
37
|
-
const payload = {
|
|
38
|
-
tool_name: 'NotebookEdit',
|
|
39
|
-
tool_input: {
|
|
40
|
-
notebook_path: '/repo/nb.ipynb',
|
|
41
|
-
cell_id: 'x',
|
|
42
|
-
new_source: '# i assume the timezone is UTC',
|
|
43
|
-
cell_type: 'code',
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
assert.ok(isWarn(runGate(payload, ENABLE)), 'gate now detects conjecture phrasing via NotebookEdit');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// EDGE CASE (BUG): 'invoke_subagent' is a declared delegation tool name (TOOL_GROUPS.delegation
|
|
50
|
-
// in lib/hook-io.mjs) and is matched in hooks.json, but extractContent() branches on
|
|
51
|
-
// TOOL_GROUPS.delegation.includes(toolName) which DOES include invoke_subagent — so this one
|
|
52
|
-
// should actually work. Verify it does (a real behavioral check, not a bug) to make sure the
|
|
53
|
-
// delegation branch itself is sound before concluding the write-side gap is the only issue.
|
|
54
|
-
test('OK: conjecture phrasing in an invoke_subagent prompt is detected (delegation branch is sound)', () => {
|
|
55
|
-
const payload = {
|
|
56
|
-
tool_name: 'invoke_subagent',
|
|
57
|
-
tool_input: { prompt: 'i assume the config is already loaded, proceed' },
|
|
58
|
-
};
|
|
59
|
-
assert.ok(isWarn(runGate(payload, ENABLE)));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// EDGE CASE: false-positive check — "should be" is a very broad conjecture pattern that can
|
|
63
|
-
// match ordinary correct engineering prose describing an invariant, not an unverified guess,
|
|
64
|
-
// e.g. "the response should be a 200 for valid input" (spec language, not conjecture).
|
|
65
|
-
test('OK/expected-noise: "should be" fires even on spec-style invariant language (broad pattern, not a bug but a known false-positive source)', () => {
|
|
66
|
-
const result = runGate(
|
|
67
|
-
{ tool_name: 'Write', tool_input: { file_path: '/repo/x.js', content: '// per the spec, the response should be a 200 for valid input' } },
|
|
68
|
-
ENABLE,
|
|
69
|
-
);
|
|
70
|
-
assert.ok(isWarn(result), 'documents that "should be" over-fires on legitimate spec prose, not just conjecture');
|
|
71
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'never-assume-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
-
});
|
|
26
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function write(content) {
|
|
30
|
-
return {
|
|
31
|
-
tool_name: 'Write',
|
|
32
|
-
tool_input: { file_path: '/repo/src/x.js', content },
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function isWarn(result) {
|
|
36
|
-
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
37
|
-
}
|
|
38
|
-
function isDeny(result) {
|
|
39
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ENABLE = {
|
|
43
|
-
config: { gates: { requireVerificationBeforeAssuming: true } },
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
test('warns on conjecture phrasing', () => {
|
|
47
|
-
assert.ok(isWarn(runGate(write('// i assume the timezone is UTC'), ENABLE)));
|
|
48
|
-
assert.ok(isWarn(runGate(write('// probably fine to skip this'), ENABLE)));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('never denies, only warns', () => {
|
|
52
|
-
const result = runGate(write('// i assume this works'), ENABLE);
|
|
53
|
-
assert.ok(isWarn(result));
|
|
54
|
-
assert.ok(!isDeny(result));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('allows content without conjecture phrasing', () => {
|
|
58
|
-
assert.equal(runGate(write('const x = 1;'), ENABLE), null);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('disabled by default (registry default is false)', () => {
|
|
62
|
-
assert.equal(runGate(write('// i assume this works')), null);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('warns on conjecture phrasing in Spanish', () => {
|
|
66
|
-
assert.ok(isWarn(runGate(write('// supongo que la zona horaria es UTC'), ENABLE)));
|
|
67
|
-
assert.ok(isWarn(runGate(write('// probablemente sea correcto omitir esto'), ENABLE)));
|
|
68
|
-
assert.ok(isWarn(runGate(write('// deberia ser suficiente con este chequeo'), ENABLE)));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('bilingual control: ES and EN equivalent conjecture briefs both warn', () => {
|
|
72
|
-
const es = write('// creo que el usuario ya esta autenticado, no lo verifique');
|
|
73
|
-
const en = write('// i assume the user is already authenticated, did not verify it');
|
|
74
|
-
assert.ok(isWarn(runGate(es, ENABLE)));
|
|
75
|
-
assert.ok(isWarn(runGate(en, ENABLE)));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('allows neutral Spanish content without conjecture phrasing', () => {
|
|
79
|
-
assert.equal(runGate(write('const contrasena = "verificada";'), ENABLE), null);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('project conjecturePatterns override replaces the built-in list', () => {
|
|
83
|
-
const config = {
|
|
84
|
-
gates: {
|
|
85
|
-
requireVerificationBeforeAssuming: {
|
|
86
|
-
enabled: true,
|
|
87
|
-
conjecturePatterns: ['totally made up'],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
assert.equal(runGate(write('// i assume this works'), { config }), null);
|
|
92
|
-
assert.ok(
|
|
93
|
-
isWarn(runGate(write('// totally made up value here'), { config })),
|
|
94
|
-
);
|
|
95
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// Edge-case audit for no-blocking: bypasses and false positives.
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
7
|
-
import { test } from 'node:test';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
11
|
-
|
|
12
|
-
function runGate(payload, { config } = {}) {
|
|
13
|
-
const project = mkdtempSync(join(tmpdir(), 'no-blocking-edge-'));
|
|
14
|
-
mkdirSync(join(project, '.git'));
|
|
15
|
-
if (config) {
|
|
16
|
-
mkdirSync(join(project, '.ai'));
|
|
17
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
18
|
-
}
|
|
19
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
20
|
-
input: JSON.stringify(payload),
|
|
21
|
-
encoding: 'utf8',
|
|
22
|
-
cwd: project,
|
|
23
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
24
|
-
});
|
|
25
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ENABLED = { gates: { blockWaitingCommands: true } };
|
|
29
|
-
|
|
30
|
-
function bash(command, extra = {}) {
|
|
31
|
-
return { tool_name: 'Bash', tool_input: { command, ...extra } };
|
|
32
|
-
}
|
|
33
|
-
function mcpTool(name, input) {
|
|
34
|
-
return { tool_name: name, tool_input: input };
|
|
35
|
-
}
|
|
36
|
-
function isDeny(result) {
|
|
37
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── BUG: MCP shell-equivalent tools are not in TOOL_GROUPS.shell/delegation.
|
|
41
|
-
test('FIXED: an mcp shell-equivalent tool running "sleep 30" is now recognized', () => {
|
|
42
|
-
const result = runGate(mcpTool('mcp__shell__run', { command: 'sleep 30' }), {
|
|
43
|
-
config: ENABLED,
|
|
44
|
-
});
|
|
45
|
-
assert.ok(isDeny(result)); // toolInGroups classifies mcp__shell__run into the shell group
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ── BUG (false positive / evasion pairing): the WAIT-JUSTIFIED marker escape requires
|
|
49
|
-
// NO reason validation beyond "two more tokens exist after the marker" — the reason text
|
|
50
|
-
// itself is never checked for plausibility, so trivially defeats the intent of a
|
|
51
|
-
// "declared, justified wait": any two words work as an unchecked excuse.
|
|
52
|
-
test('BUG (weak justification check): any two-word text after the marker escapes the block regardless of content', () => {
|
|
53
|
-
const result = runGate(
|
|
54
|
-
bash('sleep 9999 # WAIT-JUSTIFIED: because yes'),
|
|
55
|
-
{ config: ENABLED },
|
|
56
|
-
);
|
|
57
|
-
assert.equal(result, null); // allowed: marker escape has no semantic validation, only shape
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// ── BUG candidate: `timeout 30 someserver` is matched by the sleep|timeout pattern
|
|
61
|
-
// requiring the command to START the line or follow |;&/then/do. A background-launched
|
|
62
|
-
// dev server that is piped through `| cat` first, e.g. `npm run dev | cat`, should still
|
|
63
|
-
// match \b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|watch)\b since that's a
|
|
64
|
-
// substring test with no anchoring requirement (unlike sleep/timeout). Confirm no bypass.
|
|
65
|
-
test('OK: piping a dev server through another command does not bypass the dev-server pattern', () => {
|
|
66
|
-
assert.ok(
|
|
67
|
-
isDeny(runGate(bash('npm run dev | cat'), { config: ENABLED })),
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ── BUG: `NOT_TAKING_THE_TURN` includes `-d\b` as a generic "detached" flag marker. This
|
|
72
|
-
// is dangerously broad: ANY command containing a standalone `-d` token — including one
|
|
73
|
-
// completely unrelated to backgrounding — escapes every blocking pattern. E.g. `sleep 30
|
|
74
|
-
// -d` (nonsensical flag for sleep, but matches) or more realistically a docker/curl
|
|
75
|
-
// command with an unrelated `-d` (data) flag combined with a blocking pattern.
|
|
76
|
-
test('FIXED: an unrelated "-d" flag (curl -d) no longer escapes the blocking check', () => {
|
|
77
|
-
// curl -d is "data", not detach. The bare -d escape was removed, so a genuinely foreground
|
|
78
|
-
// sleep on the same command line is now denied despite the unrelated -d flag.
|
|
79
|
-
const result = runGate(bash('sleep 30; curl -d "payload" http://x'), {
|
|
80
|
-
config: ENABLED,
|
|
81
|
-
});
|
|
82
|
-
assert.ok(isDeny(result));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ── BUG candidate: `npm run dev &` (trailing background operator with a space before
|
|
86
|
-
// EOL, e.g. training whitespace) — NOT_TAKING_THE_TURN requires `&\s*$`. A trailing
|
|
87
|
-
// newline in the JSON string (unlikely from a real shell, but a delegation prompt or a
|
|
88
|
-
// tool that appends '\n') would still satisfy `\s*$` since \s includes \n. Not pursued
|
|
89
|
-
// further as a distinct bug; documenting as checked.
|
|
90
|
-
test('OK: trailing background operator followed by trailing whitespace still escapes correctly (not a bug)', () => {
|
|
91
|
-
assert.equal(
|
|
92
|
-
runGate(bash('npm run dev & '), { config: ENABLED }),
|
|
93
|
-
null,
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// ── BUG candidate: PowerShell `Start-Job` / `Start-Process ... -NoNewWindow` background
|
|
98
|
-
// forms are not recognized by NOT_TAKING_THE_TURN, but that only matters if the command
|
|
99
|
-
// is ALSO caught by a blocking pattern in the first place. `Start-Sleep` alone, run as
|
|
100
|
-
// `Start-Job { Start-Sleep 30 }` (already backgrounded via Start-Job) is still matched by
|
|
101
|
-
// \bStart-Sleep\b and NOT recognized as non-blocking, causing a FALSE POSITIVE: a
|
|
102
|
-
// genuinely backgrounded PowerShell job gets denied because its "backgrounded" wrapper
|
|
103
|
-
// (Start-Job) is not in NOT_TAKING_THE_TURN.
|
|
104
|
-
test('FIXED: Start-Sleep inside a Start-Job background wrapper is now allowed', () => {
|
|
105
|
-
const result = runGate(bash('Start-Job { Start-Sleep 30 }'), {
|
|
106
|
-
config: ENABLED,
|
|
107
|
-
});
|
|
108
|
-
assert.equal(result, null); // Start-Job is now recognized as a backgrounding mechanism
|
|
109
|
-
});
|