-
Notifications
You must be signed in to change notification settings - Fork 3
test: add plugin routing tests for bundled sq plugin #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| const fs = require("fs") | ||
| const os = require("os") | ||
| const path = require("path") | ||
| const { execSync } = require("child_process") | ||
|
|
||
| const CLI = path.join(__dirname, "..", "cli", "supercli.js") | ||
|
|
||
| function runNoServer(args, options = {}) { | ||
| try { | ||
| const env = { ...process.env } | ||
| delete env.SUPERCLI_SERVER | ||
| const out = execSync(`node ${CLI} ${args}`, { | ||
| encoding: "utf-8", | ||
| timeout: 15000, | ||
| env: { ...env, ...(options.env || {}) } | ||
| }) | ||
| return { ok: true, output: out.trim(), code: 0 } | ||
| } catch (err) { | ||
| return { | ||
| ok: false, | ||
| output: (err.stdout || "").trim(), | ||
| stderr: (err.stderr || "").trim(), | ||
| code: err.status | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function writeFakeHttprobeBinary(dir) { | ||
| const bin = path.join(dir, "httprobe") | ||
| fs.writeFileSync(bin, [ | ||
| "#!/usr/bin/env node", | ||
| "console.log('https://example.com');" | ||
| ].join("\n"), "utf-8") | ||
| fs.chmodSync(bin, 0o755) | ||
| return bin | ||
| } | ||
|
|
||
| describe("httprobe plugin", () => { | ||
| const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-httprobe-")) | ||
| const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-httprobe-")) | ||
| writeFakeHttprobeBinary(fakeDir) | ||
| const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use Hardcoding
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
|
|
||
| beforeAll(() => { | ||
| runNoServer("plugins install ./plugins/httprobe --on-conflict replace --json", { env }) | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| runNoServer("plugins remove httprobe --json", { env }) | ||
| fs.rmSync(fakeDir, { recursive: true, force: true }) | ||
| fs.rmSync(tempHome, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| test("routes the domain probe command", () => { | ||
| const r = runNoServer("httprobe domain probe --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("httprobe.domain.probe") | ||
| expect(data.data.raw).toBe("https://example.com") | ||
| }) | ||
|
|
||
| test("doctor reports httprobe dependency as healthy", () => { | ||
| const r = runNoServer("plugins doctor httprobe --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.ok).toBe(true) | ||
| expect(data.checks.some(c => c.type === "binary" && c.binary === "httprobe" && c.ok === true)).toBe(true) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| const fs = require("fs") | ||
| const os = require("os") | ||
| const path = require("path") | ||
| const { execSync } = require("child_process") | ||
|
|
||
| const CLI = path.join(__dirname, "..", "cli", "supercli.js") | ||
|
|
||
| function runNoServer(args, options = {}) { | ||
| try { | ||
| const env = { ...process.env } | ||
| delete env.SUPERCLI_SERVER | ||
| const out = execSync(`node ${CLI} ${args}`, { | ||
| encoding: "utf-8", | ||
| timeout: 15000, | ||
| env: { ...env, ...(options.env || {}) } | ||
| }) | ||
| return { ok: true, output: out.trim(), code: 0 } | ||
| } catch (err) { | ||
| return { | ||
| ok: false, | ||
| output: (err.stdout || "").trim(), | ||
| stderr: (err.stderr || "").trim(), | ||
| code: err.status | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function writeFakeLazysqlBinary(dir) { | ||
| const bin = path.join(dir, "lazysql") | ||
| fs.writeFileSync(bin, [ | ||
| "#!/usr/bin/env node", | ||
| "const args = process.argv.slice(2);", | ||
| "if (args[0] === '--version') { console.log('lazysql v0.3.0-test'); process.exit(0); }", | ||
| "console.log(JSON.stringify({ ok: true, args }));" | ||
| ].join("\n"), "utf-8") | ||
| fs.chmodSync(bin, 0o755) | ||
| return bin | ||
| } | ||
|
|
||
| describe("lazysql plugin", () => { | ||
| const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-lazysql-")) | ||
| const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-lazysql-")) | ||
| writeFakeLazysqlBinary(fakeDir) | ||
| const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome } | ||
|
|
||
| beforeAll(() => { | ||
| runNoServer("plugins install ./plugins/lazysql --on-conflict replace --json", { env }) | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| runNoServer("plugins remove lazysql --json", { env }) | ||
| fs.rmSync(fakeDir, { recursive: true, force: true }) | ||
| fs.rmSync(tempHome, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| test("routes the self version command", () => { | ||
| const r = runNoServer("lazysql self version --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("lazysql.self.version") | ||
| expect(data.data.raw).toBe("lazysql v0.3.0-test") | ||
| }) | ||
|
|
||
| test("routes connection open with a positional url", () => { | ||
| const r = runNoServer("lazysql connection open postgres://x --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("lazysql.connection.open") | ||
| const inner = JSON.parse(data.data.raw) | ||
| expect(inner.args).toContain("postgres://x") | ||
| }) | ||
|
|
||
| test("supports namespace passthrough for unknown subcommands", () => { | ||
| const r = runNoServer("lazysql somecmd --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("lazysql.passthrough") | ||
| const inner = JSON.parse(data.data.raw) | ||
| expect(inner.args).toContain("somecmd") | ||
| }) | ||
|
|
||
| test("doctor reports lazysql dependency as healthy", () => { | ||
| const r = runNoServer("plugins doctor lazysql --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.ok).toBe(true) | ||
| expect(data.checks.some(c => c.type === "binary" && c.binary === "lazysql" && c.ok === true)).toBe(true) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| const fs = require("fs") | ||
| const os = require("os") | ||
| const path = require("path") | ||
| const { execSync } = require("child_process") | ||
|
|
||
| const CLI = path.join(__dirname, "..", "cli", "supercli.js") | ||
|
|
||
| function runNoServer(args, options = {}) { | ||
| try { | ||
| const env = { ...process.env } | ||
| delete env.SUPERCLI_SERVER | ||
| const out = execSync(`node ${CLI} ${args}`, { | ||
| encoding: "utf-8", | ||
| timeout: 15000, | ||
| env: { ...env, ...(options.env || {}) } | ||
| }) | ||
| return { ok: true, output: out.trim(), code: 0 } | ||
| } catch (err) { | ||
| return { | ||
| ok: false, | ||
| output: (err.stdout || "").trim(), | ||
| stderr: (err.stderr || "").trim(), | ||
| code: err.status | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function writeFakeSqBinary(dir) { | ||
| const bin = path.join(dir, "sq") | ||
| fs.writeFileSync(bin, [ | ||
| "#!/usr/bin/env node", | ||
| "const args = process.argv.slice(2);", | ||
| "if (args[0] === 'version') { console.log('sq v0.48.0-test'); process.exit(0); }", | ||
| "console.log(JSON.stringify({ ok: true, args }));" | ||
| ].join("\n"), "utf-8") | ||
| fs.chmodSync(bin, 0o755) | ||
| return bin | ||
| } | ||
|
|
||
| describe("sq plugin", () => { | ||
| const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-sq-")) | ||
| const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-sq-")) | ||
| writeFakeSqBinary(fakeDir) | ||
| const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome } | ||
|
|
||
| beforeAll(() => { | ||
| runNoServer("plugins install ./plugins/sq --on-conflict replace --json", { env }) | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| runNoServer("plugins remove sq --json", { env }) | ||
| fs.rmSync(fakeDir, { recursive: true, force: true }) | ||
| fs.rmSync(tempHome, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| test("routes a subcommand through namespace passthrough", () => { | ||
| const r = runNoServer("sq inspect --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("sq.passthrough") | ||
| expect(data.data.args).toContain("inspect") | ||
| expect(data.data.args).toContain("--json") | ||
| }) | ||
|
|
||
| test("passes multiple arguments through in order", () => { | ||
| const r = runNoServer("sq .data --format json --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.command).toBe("sq.passthrough") | ||
| expect(data.data.args).toContain(".data") | ||
| expect(data.data.args).toContain("--format") | ||
| expect(data.data.args).toContain("json") | ||
| }) | ||
|
|
||
| test("doctor reports sq dependency as healthy", () => { | ||
| const r = runNoServer("plugins doctor sq --json", { env }) | ||
| expect(r.ok).toBe(true) | ||
| const data = JSON.parse(r.output) | ||
| expect(data.ok).toBe(true) | ||
| expect(data.checks.some(c => c.type === "binary" && c.binary === "sq" && c.ok === true)).toBe(true) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Quote the CLI path to prevent errors if the directory contains spaces.
The
CLIpath is directly interpolated into theexecSyncshell command. If the project is cloned into a directory path that contains spaces (e.g.,/Users/dev/my projects/cli), the command will fail to execute.__tests__/httprobe-plugin.test.js#L12-L16: wrap${CLI}in quotes:execSync(\node "${CLI}" ${args}`, {`__tests__/lazysql-plugin.test.js#L12-L16: wrap${CLI}in quotes:execSync(\node "${CLI}" ${args}`, {`__tests__/sq-plugin.test.js#L12-L16: wrap${CLI}in quotes:execSync(\node "${CLI}" ${args}`, {`🧰 Tools
🪛 OpenGrep (1.25.0)
[ERROR] 12-16: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
📍 Affects 3 files
__tests__/httprobe-plugin.test.js#L12-L16(this comment)__tests__/lazysql-plugin.test.js#L12-L16__tests__/sq-plugin.test.js#L12-L16🤖 Prompt for AI Agents