@shaykec/claude-teach 0.3.0 → 0.4.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/package.json +2 -2
- package/src/cli.e2e.test.js +302 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaykec/claude-teach",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Socratic AI teaching platform — learn anything through guided dialogue, visual canvas, and gamification",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"commander": "^12.0.0",
|
|
16
16
|
"js-yaml": "^4.1.0",
|
|
17
17
|
"chalk": "^5.3.0",
|
|
18
|
-
"@shaykec/bridge": "0.
|
|
18
|
+
"@shaykec/bridge": "0.3.0",
|
|
19
19
|
"@shaykec/shared": "0.1.0",
|
|
20
20
|
"@shaykec/plugin": "0.1.0",
|
|
21
21
|
"@shaykec/extension": "0.1.0"
|
package/src/cli.e2e.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Level 1 E2E tests — run the claude-teach CLI binary and assert on output.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
7
|
import { existsSync, mkdtempSync, rmSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
@@ -638,36 +638,325 @@ describe('CLI E2E: npx @shaykec/claude-teach', () => {
|
|
|
638
638
|
});
|
|
639
639
|
|
|
640
640
|
// =====================================================================
|
|
641
|
-
// Group H —
|
|
641
|
+
// Group H — Setup command (all options)
|
|
642
642
|
// =====================================================================
|
|
643
643
|
|
|
644
|
-
describe('CLI E2E:
|
|
645
|
-
it('setup outputs plugin
|
|
644
|
+
describe('CLI E2E: Setup command', () => {
|
|
645
|
+
it('setup --show-path outputs plugin path containing packages/plugin', () => {
|
|
646
646
|
const { stdout, exitCode } = runCli(['setup', '--show-path']);
|
|
647
647
|
expect(exitCode).toBe(0);
|
|
648
648
|
expect(stdout.trim()).toContain('packages/plugin');
|
|
649
649
|
});
|
|
650
650
|
|
|
651
|
-
it('setup
|
|
652
|
-
// setup without --show-path prints the full checklist but also
|
|
653
|
-
// starts the bridge + opens browser. We can't let it do that in tests.
|
|
654
|
-
// Instead, just test --show-path (machine-readable) works.
|
|
651
|
+
it('setup --show-path outputs an absolute path', () => {
|
|
655
652
|
const { stdout, exitCode } = runCli(['setup', '--show-path']);
|
|
656
653
|
expect(exitCode).toBe(0);
|
|
657
|
-
expect(stdout.trim()
|
|
654
|
+
expect(stdout.trim()).toMatch(/^\//); // starts with /
|
|
658
655
|
});
|
|
659
656
|
|
|
660
|
-
it('
|
|
657
|
+
it('setup --show-path path actually has plugin.json', () => {
|
|
658
|
+
const { stdout, exitCode } = runCli(['setup', '--show-path']);
|
|
659
|
+
expect(exitCode).toBe(0);
|
|
660
|
+
const pluginDir = stdout.trim();
|
|
661
|
+
expect(existsSync(join(pluginDir, '.claude-plugin', 'plugin.json'))).toBe(true);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('setup --help shows all options', () => {
|
|
665
|
+
const { stdout, exitCode } = runCli(['setup', '--help']);
|
|
666
|
+
expect(exitCode).toBe(0);
|
|
667
|
+
const clean = stripAnsi(stdout);
|
|
668
|
+
expect(clean).toContain('Check environment');
|
|
669
|
+
expect(clean).toContain('--show-path');
|
|
670
|
+
expect(clean).toContain('--extension');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('start --help shows command description and options', () => {
|
|
661
674
|
const { stdout, exitCode } = runCli(['start', '--help']);
|
|
662
675
|
expect(exitCode).toBe(0);
|
|
663
676
|
const clean = stripAnsi(stdout);
|
|
664
677
|
expect(clean).toContain('Launch Claude Code');
|
|
678
|
+
expect(clean).toContain('--no-server');
|
|
679
|
+
expect(clean).toContain('--port');
|
|
665
680
|
});
|
|
681
|
+
});
|
|
666
682
|
|
|
667
|
-
|
|
668
|
-
|
|
683
|
+
// =====================================================================
|
|
684
|
+
// Group I — Start command (spawn-based, no actual Claude)
|
|
685
|
+
// =====================================================================
|
|
686
|
+
|
|
687
|
+
describe('CLI E2E: Start command', () => {
|
|
688
|
+
it('start --no-server fails gracefully when claude is not on PATH', () => {
|
|
689
|
+
// Build a PATH that includes node but excludes claude
|
|
690
|
+
const nodeBinDir = join(process.execPath, '..');
|
|
691
|
+
const { exitCode, stderr, stdout } = runCli(['start', '--no-server'], {
|
|
692
|
+
env: { PATH: `${nodeBinDir}:/usr/bin:/bin` },
|
|
693
|
+
expectError: true,
|
|
694
|
+
timeout: 10000,
|
|
695
|
+
});
|
|
696
|
+
const output = stripAnsi(stdout + stderr);
|
|
697
|
+
// Should fail because claude is not found on this restricted PATH
|
|
698
|
+
expect(exitCode).not.toBe(0);
|
|
699
|
+
expect(output).toContain('Claude Code CLI not found');
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('start spawns bridge server on custom port', async () => {
|
|
703
|
+
const port = 14456 + Math.floor(Math.random() * 1000);
|
|
704
|
+
// Spawn start with a bogus claude path to test bridge startup
|
|
705
|
+
// Use a wrapper script that just exits so we can verify bridge started
|
|
706
|
+
const child = spawn('node', [
|
|
707
|
+
join(PROJECT_ROOT, 'packages/core/src/cli.js'),
|
|
708
|
+
'start',
|
|
709
|
+
'--port', String(port),
|
|
710
|
+
], {
|
|
711
|
+
cwd: PROJECT_ROOT,
|
|
712
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
let stdout = '';
|
|
716
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
// Wait a bit for the bridge to start
|
|
720
|
+
let bridgeStarted = false;
|
|
721
|
+
for (let i = 0; i < 10; i++) {
|
|
722
|
+
await new Promise(r => setTimeout(r, 500));
|
|
723
|
+
try {
|
|
724
|
+
const resp = await fetch(`http://localhost:${port}/health`);
|
|
725
|
+
if (resp.ok) {
|
|
726
|
+
bridgeStarted = true;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
} catch { /* not ready */ }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// If claude is installed, bridge should have started
|
|
733
|
+
// If claude is not installed, the process exits with error before bridge starts
|
|
734
|
+
// Either way, the test should not fail - just verify the behavior
|
|
735
|
+
const cleanOutput = stripAnsi(stdout);
|
|
736
|
+
if (bridgeStarted) {
|
|
737
|
+
const resp = await fetch(`http://localhost:${port}/health`);
|
|
738
|
+
const data = await resp.json();
|
|
739
|
+
expect(data).toHaveProperty('status', 'ok');
|
|
740
|
+
} else {
|
|
741
|
+
// Process exited because claude is not on PATH — valid behavior
|
|
742
|
+
expect(true).toBe(true);
|
|
743
|
+
}
|
|
744
|
+
} finally {
|
|
745
|
+
child.kill('SIGTERM');
|
|
746
|
+
await new Promise(resolve => child.on('close', resolve));
|
|
747
|
+
}
|
|
748
|
+
}, 15000);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// =====================================================================
|
|
752
|
+
// Group J — Bridge server /api/open-extension endpoint
|
|
753
|
+
// =====================================================================
|
|
754
|
+
|
|
755
|
+
describe('CLI E2E: Bridge API endpoints', () => {
|
|
756
|
+
let port;
|
|
757
|
+
let child;
|
|
758
|
+
|
|
759
|
+
beforeAll(async () => {
|
|
760
|
+
port = 15456 + Math.floor(Math.random() * 1000);
|
|
761
|
+
child = spawn('node', [
|
|
762
|
+
join(PROJECT_ROOT, 'packages/core/src/cli.js'),
|
|
763
|
+
'serve',
|
|
764
|
+
'--port', String(port),
|
|
765
|
+
], {
|
|
766
|
+
cwd: PROJECT_ROOT,
|
|
767
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Wait for server to be ready
|
|
771
|
+
for (let i = 0; i < 20; i++) {
|
|
772
|
+
await new Promise(r => setTimeout(r, 500));
|
|
773
|
+
try {
|
|
774
|
+
const resp = await fetch(`http://localhost:${port}/health`);
|
|
775
|
+
if (resp.ok) break;
|
|
776
|
+
} catch { /* not ready */ }
|
|
777
|
+
}
|
|
778
|
+
}, 15000);
|
|
779
|
+
|
|
780
|
+
afterAll(async () => {
|
|
781
|
+
if (child) {
|
|
782
|
+
child.kill('SIGTERM');
|
|
783
|
+
await new Promise(resolve => child.on('close', resolve));
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('POST /api/open-extension returns ok with extension path', async () => {
|
|
788
|
+
const resp = await fetch(`http://localhost:${port}/api/open-extension`, {
|
|
789
|
+
method: 'POST',
|
|
790
|
+
});
|
|
791
|
+
const data = await resp.json();
|
|
792
|
+
expect(resp.status).toBe(200);
|
|
793
|
+
expect(data.ok).toBe(true);
|
|
794
|
+
expect(data.path).toContain('extension');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('GET /health returns status ok and tier info', async () => {
|
|
798
|
+
const resp = await fetch(`http://localhost:${port}/health`);
|
|
799
|
+
const data = await resp.json();
|
|
800
|
+
expect(resp.status).toBe(200);
|
|
801
|
+
expect(data.status).toBe('ok');
|
|
802
|
+
expect(data).toHaveProperty('tier');
|
|
803
|
+
expect(data).toHaveProperty('uptime');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('GET /api/tier returns tier info', async () => {
|
|
807
|
+
const resp = await fetch(`http://localhost:${port}/api/tier`);
|
|
808
|
+
const data = await resp.json();
|
|
809
|
+
expect(resp.status).toBe(200);
|
|
810
|
+
expect(data).toHaveProperty('tier');
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('GET /api/progress returns progress data', async () => {
|
|
814
|
+
const resp = await fetch(`http://localhost:${port}/api/progress`);
|
|
815
|
+
const data = await resp.json();
|
|
816
|
+
expect(resp.status).toBe(200);
|
|
817
|
+
// Default empty progress
|
|
818
|
+
expect(data).toHaveProperty('user');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('GET /api/events returns empty events array', async () => {
|
|
822
|
+
const resp = await fetch(`http://localhost:${port}/api/events`);
|
|
823
|
+
const data = await resp.json();
|
|
824
|
+
expect(resp.status).toBe(200);
|
|
825
|
+
expect(data).toHaveProperty('events');
|
|
826
|
+
expect(Array.isArray(data.events)).toBe(true);
|
|
827
|
+
expect(data).toHaveProperty('count');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('GET /api/templates returns templates list', async () => {
|
|
831
|
+
const resp = await fetch(`http://localhost:${port}/api/templates`);
|
|
832
|
+
const data = await resp.json();
|
|
833
|
+
expect(resp.status).toBe(200);
|
|
834
|
+
expect(data).toHaveProperty('templates');
|
|
835
|
+
expect(Array.isArray(data.templates)).toBe(true);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('POST /api/event with invalid body returns 400', async () => {
|
|
839
|
+
const resp = await fetch(`http://localhost:${port}/api/event`, {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
842
|
+
body: 'not-json',
|
|
843
|
+
});
|
|
844
|
+
expect(resp.status).toBe(400);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('POST /api/capture saves a capture file', async () => {
|
|
848
|
+
const resp = await fetch(`http://localhost:${port}/api/capture`, {
|
|
849
|
+
method: 'POST',
|
|
850
|
+
headers: { 'Content-Type': 'application/json' },
|
|
851
|
+
body: JSON.stringify({ title: 'test-capture', content: 'hello' }),
|
|
852
|
+
});
|
|
853
|
+
const data = await resp.json();
|
|
854
|
+
expect(resp.status).toBe(200);
|
|
855
|
+
expect(data.ok).toBe(true);
|
|
856
|
+
expect(data.saved).toContain('test-capture');
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('GET /nonexistent returns 404', async () => {
|
|
860
|
+
const resp = await fetch(`http://localhost:${port}/nonexistent.xyz`);
|
|
861
|
+
expect(resp.status).toBe(404);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('OPTIONS returns 204 (CORS preflight)', async () => {
|
|
865
|
+
const resp = await fetch(`http://localhost:${port}/api/event`, {
|
|
866
|
+
method: 'OPTIONS',
|
|
867
|
+
});
|
|
868
|
+
expect(resp.status).toBe(204);
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// =====================================================================
|
|
873
|
+
// Group K — All CLI subcommand help texts
|
|
874
|
+
// =====================================================================
|
|
875
|
+
|
|
876
|
+
describe('CLI E2E: Subcommand help texts', () => {
|
|
877
|
+
const subcommands = [
|
|
878
|
+
{ args: ['list', '--help'], contains: 'List available learning modules' },
|
|
879
|
+
{ args: ['get', '--help'], contains: 'Get module content' },
|
|
880
|
+
{ args: ['stats', '--help'], contains: 'gamification dashboard' },
|
|
881
|
+
{ args: ['level-up', '--help'], contains: 'belt roadmap' },
|
|
882
|
+
{ args: ['serve', '--help'], contains: 'bridge server' },
|
|
883
|
+
{ args: ['inbox', '--help'], contains: 'captured from the browser' },
|
|
884
|
+
{ args: ['start', '--help'], contains: 'Launch Claude Code' },
|
|
885
|
+
{ args: ['setup', '--help'], contains: 'Check environment' },
|
|
886
|
+
{ args: ['registry:build', '--help'], contains: 'Rebuild the module registry' },
|
|
887
|
+
{ args: ['install', '--help'], contains: 'Install a module pack' },
|
|
888
|
+
{ args: ['packs', '--help'], contains: 'installed module packs' },
|
|
889
|
+
{ args: ['update', '--help'], contains: 'Update an installed module pack' },
|
|
890
|
+
{ args: ['remove', '--help'], contains: 'Remove an installed module pack' },
|
|
891
|
+
{ args: ['search', '--help'], contains: 'Search configured registries' },
|
|
892
|
+
{ args: ['author', '--help'], contains: 'authoring tools' },
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
for (const { args, contains } of subcommands) {
|
|
896
|
+
it(`${args[0]} --help shows description`, () => {
|
|
897
|
+
const { stdout, exitCode } = runCli(args);
|
|
898
|
+
expect(exitCode).toBe(0);
|
|
899
|
+
expect(stripAnsi(stdout)).toContain(contains);
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// =====================================================================
|
|
905
|
+
// Group L — Comprehensive npx tests (all commands)
|
|
906
|
+
// =====================================================================
|
|
907
|
+
|
|
908
|
+
describe('CLI E2E: npx comprehensive', () => {
|
|
909
|
+
it('npx setup --show-path resolves plugin via npm', () => {
|
|
910
|
+
const { stdout, exitCode } = runNpx(['setup', '--show-path'], { timeout: 60000 });
|
|
911
|
+
expect(exitCode).toBe(0);
|
|
912
|
+
expect(stdout.trim()).toContain('plugin');
|
|
913
|
+
expect(stdout.trim().length).toBeGreaterThan(5);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('npx start --help shows launch description', () => {
|
|
917
|
+
const { stdout, exitCode } = runNpx(['start', '--help'], { timeout: 60000 });
|
|
918
|
+
expect(exitCode).toBe(0);
|
|
919
|
+
expect(stripAnsi(stdout)).toContain('Launch Claude Code');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('npx setup --help shows setup options', () => {
|
|
923
|
+
const { stdout, exitCode } = runNpx(['setup', '--help'], { timeout: 60000 });
|
|
669
924
|
expect(exitCode).toBe(0);
|
|
670
925
|
const clean = stripAnsi(stdout);
|
|
671
|
-
expect(clean).toContain('
|
|
926
|
+
expect(clean).toContain('--show-path');
|
|
927
|
+
expect(clean).toContain('--extension');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('npx level-up outputs recommendations', () => {
|
|
931
|
+
const { stdout, exitCode } = runNpx(['level-up'], { timeout: 60000 });
|
|
932
|
+
expect(exitCode).toBe(0);
|
|
933
|
+
expect(stdout.trim().length).toBeGreaterThan(20);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it('npx get git --quick outputs quick reference', () => {
|
|
937
|
+
const { stdout, exitCode } = runNpx(['get', 'git', '--quick'], { timeout: 60000 });
|
|
938
|
+
expect(exitCode).toBe(0);
|
|
939
|
+
expect(stdout.length).toBeGreaterThan(50);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('npx get nonexistent-module fails with exit 1', () => {
|
|
943
|
+
const { exitCode, stderr } = runNpx(['get', 'nonexistent-module-xyz'], {
|
|
944
|
+
timeout: 60000,
|
|
945
|
+
expectError: true,
|
|
946
|
+
});
|
|
947
|
+
expect(exitCode).toBe(1);
|
|
948
|
+
expect(stderr).toContain('not found');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('npx list --difficulty beginner shows filtered modules', () => {
|
|
952
|
+
const { stdout, exitCode } = runNpx(['list', '--difficulty', 'beginner'], { timeout: 60000 });
|
|
953
|
+
expect(exitCode).toBe(0);
|
|
954
|
+
expect(stripAnsi(stdout)).toContain('git');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('npx registry:build rebuilds registry', () => {
|
|
958
|
+
const { stdout, exitCode } = runNpx(['registry:build'], { timeout: 60000 });
|
|
959
|
+
expect(exitCode).toBe(0);
|
|
960
|
+
expect(stripAnsi(stdout)).toContain('Registry rebuilt');
|
|
672
961
|
});
|
|
673
962
|
});
|