mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix(ck): run commands from the skill directory and pass JSON through a heredoc
- SKILL.md invoked every script through ~/.claude/skills/ck, which does
not exist when ck is installed as part of the ECC plugin, so every
/ck:* command failed with MODULE_NOT_FOUND. Use ${CLAUDE_SKILL_DIR}
and tell non-Claude hosts to substitute the skill directory. Scripts
still run from the project directory, since they use it as the
project path.
- The documented SessionStart command quoted "~/...", which the shell
never expands. Use $HOME and explain the plugin-install case.
- /ck:init and /ck:save piped JSON through echo '<json>'. Any apostrophe
ends the quoted string, and init JSON carries README/package.json
text. Pass it through a quoted heredoc instead.
- session-start.mjs and shared.mjs resolve SKILL.md next to the code
(import.meta.url), and the hook expands ${CLAUDE_SKILL_DIR} in the
text it injects.
- encodeProjectPath only replaced '/', while Claude Code maps every
non-alphanumeric character to '-'. Memory entries for projects whose
path has a space or dot went to a directory Claude Code never reads.
Signed-off-by: Wen <jwcastillo@gmail.com>
(cherry picked from commit c920005c2b)
This commit is contained in:
4 files changed
+166
-22
No files matched your search
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Tests for ck (Context Keeper) path handling: skill-relative script paths,
|
||||
* heredoc JSON input, and Claude Code project directory encoding.
|
||||
*
|
||||
* Run with: node tests/skills/ck-paths.test.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const SKILL_DIR = path.join(__dirname, '..', '..', 'skills', 'ck');
|
||||
const SKILL_MD = path.join(SKILL_DIR, 'SKILL.md');
|
||||
const SESSION_START = path.join(SKILL_DIR, 'hooks', 'session-start.mjs');
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function tempEnv() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-paths-'));
|
||||
const home = path.join(root, 'home');
|
||||
const project = path.join(root, "Bob's App v2");
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
const env = { ...process.env, HOME: home, USERPROFILE: home, PWD: project };
|
||||
return { root, home, project, env };
|
||||
}
|
||||
|
||||
// Return the body of the first ```bash fence that contains `marker`.
|
||||
function bashBlock(marker) {
|
||||
const md = fs.readFileSync(SKILL_MD, 'utf8');
|
||||
const block = [...md.matchAll(/```bash\n([\s\S]*?)```/g)].map(m => m[1]).find(b => b.includes(marker));
|
||||
assert.ok(block, `no bash block containing ${marker}`);
|
||||
return block;
|
||||
}
|
||||
|
||||
function runDocumentedSave(marker, placeholder, payload, ctx) {
|
||||
const script = bashBlock(marker)
|
||||
.replaceAll('${CLAUDE_SKILL_DIR}', SKILL_DIR)
|
||||
.replace(placeholder, () => JSON.stringify(payload));
|
||||
return spawnSync('bash', ['-c', script], { cwd: ctx.project, env: ctx.env, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n=== Testing ck path handling ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (await test('encodeProjectPath maps every non-alphanumeric character to "-"', async () => {
|
||||
const { encodeProjectPath } = await import(pathToFileURL(path.join(SKILL_DIR, 'commands', 'shared.mjs')).href);
|
||||
assert.strictEqual(encodeProjectPath('/home/u/My Project_v1.2'), '-home-u-My-Project-v1-2');
|
||||
assert.strictEqual(encodeProjectPath('C:\\Users\\u\\app'), 'C--Users-u-app');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await test('SessionStart hook injects the SKILL.md next to it, with the skill dir filled in', async () => {
|
||||
const ctx = tempEnv();
|
||||
try {
|
||||
const result = spawnSync(process.execPath, [SESSION_START], {
|
||||
cwd: ctx.project, env: ctx.env, input: '{}', encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const { additionalContext } = JSON.parse(result.stdout);
|
||||
assert.ok(additionalContext.includes('# ck — Context Keeper'), 'SKILL.md was not injected');
|
||||
assert.ok(!additionalContext.includes('${CLAUDE_SKILL_DIR}'), 'skill dir placeholder left unexpanded');
|
||||
assert.ok(additionalContext.includes(path.join(SKILL_DIR, 'commands')), 'skill dir not substituted');
|
||||
} finally {
|
||||
fs.rmSync(ctx.root, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' - skipped documented bash heredoc test on Windows');
|
||||
} else if (await test('documented heredoc commands save JSON with apostrophes into the native memory dir', async () => {
|
||||
const ctx = tempEnv();
|
||||
try {
|
||||
const init = runDocumentedSave('save.mjs" --init', '<confirmed-json>', {
|
||||
name: 'bobs-app', path: ctx.project, description: "Bob's app, don't break it",
|
||||
stack: ['node'], goal: "Ship Bob's release", constraints: [],
|
||||
}, ctx);
|
||||
assert.strictEqual(init.status, 0, init.stdout + init.stderr);
|
||||
|
||||
const save = runDocumentedSave('save.mjs" <<', '<json>', {
|
||||
summary: "Fixed the user's login", leftOff: "didn't finish tests", nextSteps: ["write O'Brien's test"],
|
||||
decisions: [{ what: "use Bob's parser", why: "it's faster" }], blockers: [],
|
||||
}, ctx);
|
||||
assert.strictEqual(save.status, 0, save.stdout + save.stderr);
|
||||
|
||||
const encoded = ctx.project.replace(/[^A-Za-z0-9]/g, '-');
|
||||
const memDir = path.join(ctx.home, '.claude', 'projects', encoded, 'memory');
|
||||
const files = fs.existsSync(memDir) ? fs.readdirSync(memDir) : [];
|
||||
assert.strictEqual(files.length, 1, `expected one memory entry in ${memDir}`);
|
||||
assert.ok(fs.readFileSync(path.join(memDir, files[0]), 'utf8').includes("Fixed the user's login"));
|
||||
} finally {
|
||||
fs.rmSync(ctx.root, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exitCode = failed > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in new issue
Block a user