Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ $Platforms = [ordered]@{
pi = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill' }
openclaw = @{ Target = (Join-Path $HOME '.openclaw\skills'); Style = 'folder' }
antigravity = @{ Target = (Join-Path $HOME '.gemini\antigravity\skills'); Style = 'folder' }
vibe = @{ Target = (Join-Path $HOME '.vibe\skills'); Style = 'per-skill' }
vscode = @{ Target = (Join-Path $HOME '.copilot\skills'); Style = 'per-skill' }
hermes = @{ Target = (Join-Path $HOME '.hermes\skills'); Style = 'folder' }
cline = @{ Target = (Join-Path $HOME '.cline\skills'); Style = 'folder' }
Expand Down
122 changes: 122 additions & 0 deletions tests/install/platform-table-consistency.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '../..');

const installSh = readFileSync(resolve(repoRoot, 'install.sh'), 'utf-8');
const installPs1 = readFileSync(resolve(repoRoot, 'install.ps1'), 'utf-8');
const readme = readFileSync(resolve(repoRoot, 'README.md'), 'utf-8');

/**
* Parse the platforms_table() heredoc in install.sh:
* id|$HOME/target/dir|style
*/
function parseShPlatforms(source) {
const heredoc = source.match(/platforms_table\(\)\s*\{\s*\n\s*cat <<EOF\n([\s\S]*?)\nEOF/);
if (!heredoc) return [];
const rows = [];
for (const line of heredoc[1].split('\n')) {
const m = line.match(/^([a-z0-9][a-z0-9-]*)\|([^|]+)\|(per-skill|folder)$/);
if (m) rows.push({ id: m[1], target: m[2], style: m[3] });
}
return rows;
}

/**
* Parse the $Platforms ordered hashtable in install.ps1:
* id = @{ Target = (Join-Path $HOME 'target\dir'); Style = 'style' }
*/
function parsePs1Platforms(source) {
const block = source.match(/\$Platforms\s*=\s*\[ordered\]@\{\r?\n([\s\S]*?)\r?\n\}/);
if (!block) return [];
const rows = [];
for (const line of block[1].split('\n')) {
const m = line.match(
/^\s*([a-z0-9][a-z0-9-]*)\s*=\s*@\{\s*Target\s*=\s*\(Join-Path \$HOME '([^']+)'\);\s*Style\s*=\s*'(per-skill|folder)'\s*\}/,
);
if (m) rows.push({ id: m[1], target: m[2], style: m[3] });
}
return rows;
}

/**
* Normalize a skills target dir for cross-script comparison: drop the
* home-dir prefix (`$HOME/` in bash; PowerShell targets are already relative
* to $HOME via Join-Path) and unify path separators.
*/
function normalizeTarget(target) {
return target.replace(/^\$HOME\//, '').replace(/\\/g, '/');
}

/** Backtick-quoted ids on the "Supported `<platform>` values:" README line. */
function parseReadmeSupportedValues(source) {
const line = source.match(/^- Supported `<platform>` values: (.+)$/m);
if (!line) return [];
return [...line[1].matchAll(/`([a-z0-9][a-z0-9-]*)`/g)].map((m) => m[1]);
}

/** Ids referenced as `install.sh <id>` in the Platform Compatibility table. */
function parseReadmeCompatTableIds(source) {
const section = source.match(/### Platform Compatibility\n([\s\S]*?)\n#{2,3} /);
if (!section) return [];
return [...section[1].matchAll(/`install\.sh ([a-z0-9][a-z0-9-]*)`/g)].map((m) => m[1]);
}

const shRows = parseShPlatforms(installSh);
const ps1Rows = parsePs1Platforms(installPs1);

describe('installer platform table consistency', () => {
// Guard against the parsers silently matching nothing (e.g. after a
// formatting change in either script): a regex mismatch must fail loudly
// here, not let the comparison tests pass vacuously on two empty lists.
it('parses a plausible number of platforms from both scripts', () => {
expect(shRows.length).toBeGreaterThanOrEqual(10);

Check failure on line 76 in tests/install/platform-table-consistency.test.mjs

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

tests/install/platform-table-consistency.test.mjs > installer platform table consistency > parses a plausible number of platforms from both scripts

AssertionError: expected 0 to be greater than or equal to 10 ❯ tests/install/platform-table-consistency.test.mjs:76:27
expect(ps1Rows.length).toBeGreaterThanOrEqual(10);
});

it('install.sh and install.ps1 define the same platform ids in the same order', () => {
// Same order matters, not just the same set: both scripts number their
// interactive platform menus from the table order, so "3) opencode" must
// mean the same thing on macOS/Linux and on Windows.
expect(ps1Rows.map((r) => r.id)).toEqual(shRows.map((r) => r.id));

Check failure on line 84 in tests/install/platform-table-consistency.test.mjs

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

tests/install/platform-table-consistency.test.mjs > installer platform table consistency > install.sh and install.ps1 define the same platform ids in the same order

AssertionError: expected [ Array(14) ] to deeply equal [] - Expected + Received - [] + [ + "gemini", + "codex", + "opencode", + "pi", + "openclaw", + "antigravity", + "vibe", + "vscode", + "hermes", + "cline", + "kimi", + "trae", + "nanobot", + "kiro", + ] ❯ tests/install/platform-table-consistency.test.mjs:84:38
});

it('each platform has the same link style in both scripts', () => {
const ps1ById = new Map(ps1Rows.map((r) => [r.id, r]));
for (const row of shRows) {
expect(ps1ById.get(row.id)?.style, `style for "${row.id}"`).toBe(row.style);
}
});

it('each platform has the same skills target dir in both scripts', () => {
const ps1ById = new Map(ps1Rows.map((r) => [r.id, r]));
for (const row of shRows) {
const ps1Row = ps1ById.get(row.id);
if (!ps1Row) continue; // id-set mismatch is reported by the test above
expect(normalizeTarget(ps1Row.target), `target for "${row.id}"`).toBe(
normalizeTarget(row.target),
);
}
});

it('README "Supported <platform> values" line matches the installer table', () => {
const readmeIds = parseReadmeSupportedValues(readme);
expect(readmeIds.length).toBeGreaterThanOrEqual(10);
expect([...readmeIds].sort()).toEqual(shRows.map((r) => r.id).sort());

Check failure on line 108 in tests/install/platform-table-consistency.test.mjs

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

tests/install/platform-table-consistency.test.mjs > installer platform table consistency > README "Supported <platform> values" line matches the installer table

AssertionError: expected [ 'antigravity', 'cline', …(12) ] to deeply equal [] - Expected + Received - [] + [ + "antigravity", + "cline", + "codex", + "gemini", + "hermes", + "kimi", + "kiro", + "nanobot", + "openclaw", + "opencode", + "pi", + "trae", + "vibe", + "vscode", + ] ❯ tests/install/platform-table-consistency.test.mjs:108:35
});

it('README Platform Compatibility table only references real installer platforms', () => {
// The table may legitimately document a platform via another install
// method (e.g. vscode → auto-discovery), so this is a subset check; full
// coverage of the id list is enforced by the supported-values test above.
const tableIds = parseReadmeCompatTableIds(readme);
expect(tableIds.length).toBeGreaterThanOrEqual(10);

Check failure on line 116 in tests/install/platform-table-consistency.test.mjs

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

tests/install/platform-table-consistency.test.mjs > installer platform table consistency > README Platform Compatibility table only references real installer platforms

AssertionError: expected 0 to be greater than or equal to 10 ❯ tests/install/platform-table-consistency.test.mjs:116:29
const shIds = new Set(shRows.map((r) => r.id));
for (const id of tableIds) {
expect(shIds.has(id), `"install.sh ${id}" in README compatibility table`).toBe(true);
}
});
});
Loading