mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
fix: harden error handling, fix TOCTOU races, and improve test accuracy
Core library fixes: - session-manager.js: wrap all statSync calls in try-catch to prevent TOCTOU crashes when files are deleted between readdir and stat - session-manager.js: use birthtime||ctime fallback for Linux compat - session-manager.js: remove redundant existsSync before readFile - utils.js: fix findFiles TOCTOU race on statSync inside readdir loop Hook improvements: - Add 1MB stdin buffer limits to all PostToolUse hooks to prevent unbounded memory growth from large payloads - suggest-compact.js: use fd-based atomic read+write for counter file to reduce race window between concurrent invocations - session-end.js: log when transcript file is missing, check replaceInFile return value for failed timestamp updates - start-observer.sh: log claude CLI failures instead of silently swallowing them, check observations file exists before analysis Test fixes: - Fix blocking hook tests to send matching input (dev server command) and expect correct exit code 2 instead of 1
This commit is contained in:
@@ -74,7 +74,10 @@ case "${1:-start}" in
|
|||||||
trap 'rm -f "$PID_FILE"; exit 0' TERM INT
|
trap 'rm -f "$PID_FILE"; exit 0' TERM INT
|
||||||
|
|
||||||
analyze_observations() {
|
analyze_observations() {
|
||||||
# Only analyze if we have enough observations
|
# Only analyze if observations file exists and has enough entries
|
||||||
|
if [ ! -f "$OBSERVATIONS_FILE" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
||||||
if [ "$obs_count" -lt 10 ]; then
|
if [ "$obs_count" -lt 10 ]; then
|
||||||
return
|
return
|
||||||
@@ -85,16 +88,20 @@ case "${1:-start}" in
|
|||||||
# Use Claude Code with Haiku to analyze observations
|
# Use Claude Code with Haiku to analyze observations
|
||||||
# This spawns a quick analysis session
|
# This spawns a quick analysis session
|
||||||
if command -v claude &> /dev/null; then
|
if command -v claude &> /dev/null; then
|
||||||
claude --model haiku --max-turns 3 --print \
|
if ! claude --model haiku --max-turns 3 --print \
|
||||||
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
|
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
|
||||||
>> "$LOG_FILE" 2>&1 || true
|
>> "$LOG_FILE" 2>&1; then
|
||||||
|
echo "[$(date)] Claude analysis failed (exit $?)" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[$(date)] claude CLI not found, skipping analysis" >> "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Archive processed observations
|
# Archive processed observations
|
||||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
archive_dir="${CONFIG_DIR}/observations.archive"
|
||||||
mkdir -p "$archive_dir"
|
mkdir -p "$archive_dir"
|
||||||
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl"
|
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl" 2>/dev/null || true
|
||||||
touch "$OBSERVATIONS_FILE"
|
touch "$OBSERVATIONS_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,13 @@ const EXCLUDED_PATTERNS = [
|
|||||||
/__mocks__\//,
|
/__mocks__\//,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
data += chunk;
|
if (data.length < MAX_STDIN) {
|
||||||
|
data += chunk;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
|||||||
@@ -11,10 +11,13 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
data += chunk;
|
if (data.length < MAX_STDIN) {
|
||||||
|
data += chunk;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
|||||||
@@ -11,10 +11,13 @@
|
|||||||
const { execFileSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
data += chunk;
|
if (data.length < MAX_STDIN) {
|
||||||
|
data += chunk;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ const { execFileSync } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
data += chunk;
|
if (data.length < MAX_STDIN) {
|
||||||
|
data += chunk;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
|||||||
@@ -99,17 +99,24 @@ async function main() {
|
|||||||
const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
||||||
let summary = null;
|
let summary = null;
|
||||||
|
|
||||||
if (transcriptPath && fs.existsSync(transcriptPath)) {
|
if (transcriptPath) {
|
||||||
summary = extractSessionSummary(transcriptPath);
|
if (fs.existsSync(transcriptPath)) {
|
||||||
|
summary = extractSessionSummary(transcriptPath);
|
||||||
|
} else {
|
||||||
|
log(`[SessionEnd] Transcript not found: ${transcriptPath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(sessionFile)) {
|
if (fs.existsSync(sessionFile)) {
|
||||||
// Update existing session file
|
// Update existing session file
|
||||||
replaceInFile(
|
const updated = replaceInFile(
|
||||||
sessionFile,
|
sessionFile,
|
||||||
/\*\*Last Updated:\*\*.*/,
|
/\*\*Last Updated:\*\*.*/,
|
||||||
`**Last Updated:** ${currentTime}`
|
`**Last Updated:** ${currentTime}`
|
||||||
);
|
);
|
||||||
|
if (!updated) {
|
||||||
|
log(`[SessionEnd] Failed to update timestamp in ${sessionFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a new summary and the file still has the blank template, replace it
|
// If we have a new summary and the file still has the blank template, replace it
|
||||||
if (summary) {
|
if (summary) {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
* - Compact after completing a milestone, before starting next
|
* - Compact after completing a milestone, before starting next
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const {
|
const {
|
||||||
getTempDir,
|
getTempDir,
|
||||||
readFile,
|
|
||||||
writeFile,
|
writeFile,
|
||||||
log
|
log
|
||||||
} = require('../lib/utils');
|
} = require('../lib/utils');
|
||||||
@@ -32,16 +32,28 @@ async function main() {
|
|||||||
let count = 1;
|
let count = 1;
|
||||||
|
|
||||||
// Read existing count or start at 1
|
// Read existing count or start at 1
|
||||||
const existing = readFile(counterFile);
|
// Use fd-based read+write to reduce (but not eliminate) race window
|
||||||
if (existing) {
|
// between concurrent hook invocations
|
||||||
const parsed = parseInt(existing.trim(), 10);
|
try {
|
||||||
// Guard against NaN from corrupted counter file
|
const fd = fs.openSync(counterFile, 'a+');
|
||||||
count = Number.isFinite(parsed) ? parsed + 1 : 1;
|
try {
|
||||||
|
const buf = Buffer.alloc(64);
|
||||||
|
const bytesRead = fs.readSync(fd, buf, 0, 64, 0);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10);
|
||||||
|
count = Number.isFinite(parsed) ? parsed + 1 : 1;
|
||||||
|
}
|
||||||
|
// Truncate and write new value
|
||||||
|
fs.ftruncateSync(fd, 0);
|
||||||
|
fs.writeSync(fd, String(count), 0);
|
||||||
|
} finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback: just use writeFile if fd operations fail
|
||||||
|
writeFile(counterFile, String(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated count
|
|
||||||
writeFile(counterFile, String(count));
|
|
||||||
|
|
||||||
// Suggest compact after threshold tool calls
|
// Suggest compact after threshold tool calls
|
||||||
if (count === threshold) {
|
if (count === threshold) {
|
||||||
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
|
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ function getSessionPath(filename) {
|
|||||||
* @returns {string|null} Session content or null if not found
|
* @returns {string|null} Session content or null if not found
|
||||||
*/
|
*/
|
||||||
function getSessionContent(sessionPath) {
|
function getSessionContent(sessionPath) {
|
||||||
if (!fs.existsSync(sessionPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return readFile(sessionPath);
|
return readFile(sessionPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +213,14 @@ function getAllSessions(options = {}) {
|
|||||||
|
|
||||||
const sessionPath = path.join(sessionsDir, filename);
|
const sessionPath = path.join(sessionsDir, filename);
|
||||||
|
|
||||||
// Get file stats
|
// Get file stats (wrapped in try-catch to handle TOCTOU race where
|
||||||
const stats = fs.statSync(sessionPath);
|
// file is deleted between readdirSync and statSync)
|
||||||
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(sessionPath);
|
||||||
|
} catch {
|
||||||
|
continue; // File was deleted between readdir and stat
|
||||||
|
}
|
||||||
|
|
||||||
sessions.push({
|
sessions.push({
|
||||||
...metadata,
|
...metadata,
|
||||||
@@ -226,7 +228,7 @@ function getAllSessions(options = {}) {
|
|||||||
hasContent: stats.size > 0,
|
hasContent: stats.size > 0,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
modifiedTime: stats.mtime,
|
modifiedTime: stats.mtime,
|
||||||
createdTime: stats.birthtime
|
createdTime: stats.birthtime || stats.ctime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +280,19 @@ function getSessionById(sessionId, includeContent = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionPath = path.join(sessionsDir, filename);
|
const sessionPath = path.join(sessionsDir, filename);
|
||||||
const stats = fs.statSync(sessionPath);
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(sessionPath);
|
||||||
|
} catch {
|
||||||
|
return null; // File was deleted between readdir and stat
|
||||||
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
...metadata,
|
...metadata,
|
||||||
sessionPath,
|
sessionPath,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
modifiedTime: stats.mtime,
|
modifiedTime: stats.mtime,
|
||||||
createdTime: stats.birthtime
|
createdTime: stats.birthtime || stats.ctime
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeContent) {
|
if (includeContent) {
|
||||||
@@ -319,11 +326,12 @@ function getSessionTitle(sessionPath) {
|
|||||||
* @returns {string} Formatted size (e.g., "1.2 KB")
|
* @returns {string} Formatted size (e.g., "1.2 KB")
|
||||||
*/
|
*/
|
||||||
function getSessionSize(sessionPath) {
|
function getSessionSize(sessionPath) {
|
||||||
if (!fs.existsSync(sessionPath)) {
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(sessionPath);
|
||||||
|
} catch {
|
||||||
return '0 B';
|
return '0 B';
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = fs.statSync(sessionPath);
|
|
||||||
const size = stats.size;
|
const size = stats.size;
|
||||||
|
|
||||||
if (size < 1024) return `${size} B`;
|
if (size < 1024) return `${size} B`;
|
||||||
@@ -387,7 +395,11 @@ function deleteSession(sessionPath) {
|
|||||||
* @returns {boolean} True if session exists
|
* @returns {boolean} True if session exists
|
||||||
*/
|
*/
|
||||||
function sessionExists(sessionPath) {
|
function sessionExists(sessionPath) {
|
||||||
return fs.existsSync(sessionPath) && fs.statSync(sessionPath).isFile();
|
try {
|
||||||
|
return fs.statSync(sessionPath).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -168,14 +168,19 @@ function findFiles(dir, pattern, options = {}) {
|
|||||||
const fullPath = path.join(currentDir, entry.name);
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
if (entry.isFile() && regex.test(entry.name)) {
|
if (entry.isFile() && regex.test(entry.name)) {
|
||||||
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(fullPath);
|
||||||
|
} catch {
|
||||||
|
continue; // File deleted between readdir and stat
|
||||||
|
}
|
||||||
|
|
||||||
if (maxAge !== null) {
|
if (maxAge !== null) {
|
||||||
const stats = fs.statSync(fullPath);
|
|
||||||
const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
|
const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
|
||||||
if (ageInDays <= maxAge) {
|
if (ageInDays <= maxAge) {
|
||||||
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const stats = fs.statSync(fullPath);
|
|
||||||
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
||||||
}
|
}
|
||||||
} else if (entry.isDirectory() && recursive) {
|
} else if (entry.isDirectory() && recursive) {
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ case "${1:-start}" in
|
|||||||
trap 'rm -f "$PID_FILE"; exit 0' TERM INT
|
trap 'rm -f "$PID_FILE"; exit 0' TERM INT
|
||||||
|
|
||||||
analyze_observations() {
|
analyze_observations() {
|
||||||
# Only analyze if we have enough observations
|
# Only analyze if observations file exists and has enough entries
|
||||||
|
if [ ! -f "$OBSERVATIONS_FILE" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
||||||
if [ "$obs_count" -lt 10 ]; then
|
if [ "$obs_count" -lt 10 ]; then
|
||||||
return
|
return
|
||||||
@@ -85,16 +88,20 @@ case "${1:-start}" in
|
|||||||
# Use Claude Code with Haiku to analyze observations
|
# Use Claude Code with Haiku to analyze observations
|
||||||
# This spawns a quick analysis session
|
# This spawns a quick analysis session
|
||||||
if command -v claude &> /dev/null; then
|
if command -v claude &> /dev/null; then
|
||||||
claude --model haiku --max-turns 3 --print \
|
if ! claude --model haiku --max-turns 3 --print \
|
||||||
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
|
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
|
||||||
>> "$LOG_FILE" 2>&1 || true
|
>> "$LOG_FILE" 2>&1; then
|
||||||
|
echo "[$(date)] Claude analysis failed (exit $?)" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[$(date)] claude CLI not found, skipping analysis" >> "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Archive processed observations
|
# Archive processed observations
|
||||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
archive_dir="${CONFIG_DIR}/observations.archive"
|
||||||
mkdir -p "$archive_dir"
|
mkdir -p "$archive_dir"
|
||||||
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl"
|
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl" 2>/dev/null || true
|
||||||
touch "$OBSERVATIONS_FILE"
|
touch "$OBSERVATIONS_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ async function runTests() {
|
|||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('blocking hooks output BLOCKED message', async () => {
|
if (await asyncTest('blocking hooks output BLOCKED message', async () => {
|
||||||
// Test the dev server blocking hook
|
// Test the dev server blocking hook — must send a matching command
|
||||||
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
||||||
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
||||||
|
|
||||||
@@ -248,6 +248,10 @@ async function runTests() {
|
|||||||
let code = null;
|
let code = null;
|
||||||
proc.stderr.on('data', data => stderr += data);
|
proc.stderr.on('data', data => stderr += data);
|
||||||
|
|
||||||
|
// Send a dev server command so the hook triggers the block
|
||||||
|
proc.stdin.write(JSON.stringify({
|
||||||
|
tool_input: { command: 'npm run dev' }
|
||||||
|
}));
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
|
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
@@ -258,7 +262,7 @@ async function runTests() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
||||||
assert.strictEqual(code, 1, 'Blocking hook should exit with code 1');
|
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -271,8 +275,8 @@ async function runTests() {
|
|||||||
assert.strictEqual(result.code, 0, 'Non-blocking hook should exit 0');
|
assert.strictEqual(result.code, 0, 'Non-blocking hook should exit 0');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('blocking hooks exit with code 1', async () => {
|
if (await asyncTest('blocking hooks exit with code 2', async () => {
|
||||||
// The dev server blocker always blocks
|
// The dev server blocker blocks when a dev server command is detected
|
||||||
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
||||||
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
||||||
|
|
||||||
@@ -281,6 +285,9 @@ async function runTests() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let code = null;
|
let code = null;
|
||||||
|
proc.stdin.write(JSON.stringify({
|
||||||
|
tool_input: { command: 'yarn dev' }
|
||||||
|
}));
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
|
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
@@ -290,7 +297,7 @@ async function runTests() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(code, 1, 'Blocking hook should exit 1');
|
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('hooks handle missing files gracefully', async () => {
|
if (await asyncTest('hooks handle missing files gracefully', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user