diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6a252915..26af7206 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -27,7 +27,17 @@ jobs: - name: Build (cross-platform — no POSIX rm/chmod) run: npm run build - name: Unit tests - run: npm test + continue-on-error: true + shell: bash + run: | + set -o pipefail + npm test 2>&1 | tee win-tests.log + - name: Upload Windows test log (full output — gh logs truncate) + if: always() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: windows-test-log + path: win-tests.log - name: kit smoke (init / check) — gaps surface here, non-blocking continue-on-error: true run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 159c1d8e..599052d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +## [1.29.1] - 2026-06-24 + +### Fixed + +- **Tests run on native Windows (#43).** The `test` script set env via POSIX inline vars (`KIT_NON_INTERACTIVE=1 … node`), which Windows cmd/pwsh can't parse → the suite never started. Replaced with a no-dep `scripts/test.mjs` (sets env in-process, collects `dist/**/*.test.js` itself — no shell-glob — runs `node --test`). Also: `secrets-sync` used a literal `/dev/null` (→ `D:\dev\null` ENOENT on Windows) → now `os.devNull`. +- **Windows probe is diagnosable.** The `windows-latest` workflow now tees the test output to a downloadable artifact (gh's CI logs truncate it), with `pipefail` + `continue-on-error`. + +### Notes + +- Real `windows-latest` status after this: builds ✓, **1526/1542 unit tests pass** (was: tests couldn't start). The remaining 15 are characterized on #43 — POSIX-path/`startsWith("/")` test assumptions + chmod/`0o600` permission semantics (the latter needs a Windows-ACL decision). + ## [1.29.0] - 2026-06-24 ### Added diff --git a/package-lock.json b/package-lock.json index 60861262..bb2b090b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sandstream-kit", - "version": "1.29.0", + "version": "1.29.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sandstream-kit", - "version": "1.29.0", + "version": "1.29.1", "license": "MIT", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index 445f3350..96f8cdad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sandstream-kit", - "version": "1.29.0", + "version": "1.29.1", "description": "developer kit. zero LLM, local-first, multi-vault. one command from git clone to working dev environment.", "license": "MIT", "funding": "https://buymeacoffee.com/sandstream", @@ -30,7 +30,7 @@ "build:prod": "npm run build --workspace=packages/adapter-sdk && npm run build --workspace=packages/kit-plugin-railway && npm run build --workspace=packages/kit-plugin-supabase && npm run build --workspace=packages/kit-plugin-vercel && npm run build --workspace=packages/kit-plugin-github && npm run build --workspace=packages/kit-plugin-stripe && npm run build --workspace=packages/kit-plugin-fly && npm run build --workspace=packages/kit-plugin-cloudflare && npm run build --workspace=packages/kit-plugin-snyk && npm run build --workspace=packages/kit-plugin-wiz && npm run build --workspace=packages/kit-plugin-sentry && node scripts/clean-dist.mjs && tsc -p tsconfig.prod.json && node scripts/chmod-cli.mjs", "prepublishOnly": "npm run build:prod", "dev": "tsx src/cli.ts", - "test": "KIT_NON_INTERACTIVE=1 KIT_BUMBLEBEE=0 KIT_NO_FAILURE_SIM=1 KIT_NO_UPDATE_CHECK=1 node --test --test-timeout=180000 --test-concurrency=2 dist/*.test.js dist/**/*.test.js", + "test": "node scripts/test.mjs", "lint": "eslint src", "format": "prettier --write \"**/*.ts\"", "format:check": "prettier --check \"**/*.ts\"" diff --git a/scripts/test.mjs b/scripts/test.mjs new file mode 100644 index 00000000..9b2046b5 --- /dev/null +++ b/scripts/test.mjs @@ -0,0 +1,45 @@ +// Cross-platform test runner (#43). POSIX inline env-vars (`FOO=1 node …`) don't +// work in Windows cmd/pwsh — they're parsed as a command and fail. So set the env +// here, collect the compiled test files ourselves (no shell-glob dependency, which +// also differs across shells), and invoke `node --test`. No external dep. +import { spawnSync } from "node:child_process"; +import { readdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const env = { + ...process.env, + KIT_NON_INTERACTIVE: "1", + KIT_BUMBLEBEE: "0", + KIT_NO_FAILURE_SIM: "1", + KIT_NO_UPDATE_CHECK: "1", +}; + +if (!existsSync("dist")) { + console.error("dist/ not found — run `npm run build` first"); + process.exit(1); +} + +// Recursively collect compiled .test.js files under dist/ (deterministic; no +// shell/library glob expansion involved). +function collect(dir) { + const out = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) out.push(...collect(p)); + else if (entry.name.endsWith(".test.js")) out.push(p); + } + return out; +} + +const files = collect("dist"); +if (files.length === 0) { + console.error("no dist/**/*.test.js files found"); + process.exit(1); +} + +const result = spawnSync( + process.execPath, + ["--test", "--test-timeout=180000", "--test-concurrency=2", ...files], + { stdio: "inherit", env }, +); +process.exit(result.status ?? 1); diff --git a/src/secrets-sync.ts b/src/secrets-sync.ts index 4ddaa222..ad7bb4cc 100644 --- a/src/secrets-sync.ts +++ b/src/secrets-sync.ts @@ -1,5 +1,6 @@ import { writeFile } from "node:fs/promises"; import { resolve } from "node:path"; +import { devNull } from "node:os"; import type { SecretsConfig } from "./config.js"; import { generateSecrets } from "./secrets.js"; import { exec } from "./utils/exec.js"; @@ -32,8 +33,10 @@ export async function syncSecrets( ): Promise { const { target, dryRun = false, projectPath = process.cwd() } = options; - // Resolve all secrets using the existing generate logic (reads from stores) - const { results } = await generateSecrets(secrets, "/dev/null"); + // Resolve all secrets using the existing generate logic (reads from stores). + // Discard the written output to the platform null device (os.devNull): a literal + // "/dev/null" becomes "D:\dev\null" on Windows → ENOENT (#43). + const { results } = await generateSecrets(secrets, devNull); const resolved: Record = {}; for (const r of results) {