Skip to content

Commit 2689dac

Browse files
committed
Enable cross-platform E2E tests (Windows, Linux, macOS)
- Remove the Windows exclusion guard from E2E test step in CI workflow - The process-isolated child-process harness uses only cross-platform Node.js APIs (spawn, readline, stdio pipes) and is verified to work on Windows - Add .gitattributes to enforce LF line endings on snapshot golden files - Add normalizeEOL() helper in snapshot tests for Windows CRLF handling - Add E2E_TIMEOUT_MS env var support for configurable test timeouts - Add engines.node >=22 to package.json - Add tsconfig.json for type checking - Update CI documentation in CONTRIBUTING.md Closes #12
1 parent 1841b0b commit 2689dac

7 files changed

Lines changed: 129 additions & 3 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/__snapshots__/**/*.txt text eol=lf

.github/workflows/test.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Cross-platform CI for pi-agenticoding
2+
#
3+
# Runs the full unit suite on Linux, macOS, and Windows
4+
# on the minimum Node.js version required by pi coding agent. Snapshot
5+
# tests verify TUI render output against golden files.
6+
7+
name: test
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
on:
17+
push:
18+
branches: [main]
19+
paths-ignore: ['*.md', '**/docs/**']
20+
pull_request:
21+
branches: [main]
22+
paths-ignore: ['*.md', '**/docs/**']
23+
24+
jobs:
25+
# ── Quick-check gate — catch trivial failures before the full matrix ──
26+
quick-check:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
31+
with:
32+
node-version: "22"
33+
cache: "npm"
34+
- run: npm ci
35+
36+
# Fast pre-flight checks — type errors and security issues before matrix
37+
- name: Type check
38+
run: npx tsc --noEmit
39+
40+
- name: Security audit
41+
run: npm audit --audit-level=moderate
42+
43+
# ── Cross-platform test matrix ──────────────────────────────────────
44+
# Node 22 (minimum) is tested only on Linux — the primary platform and the only one
45+
# guaranteed to have the oldest toolchain. macOS and Windows test Node 24 (latest)
46+
# to catch regressions in the newest runtime. This asymmetry is intentional: it
47+
# balances CI cost with meaningful coverage while ensuring the minimum version works
48+
# correctly on the platform most likely to encounter toolchain edge cases.
49+
test:
50+
needs: quick-check
51+
runs-on: ${{ matrix.os }}
52+
strategy:
53+
fail-fast: false # report every combination, don't cancel
54+
matrix:
55+
include:
56+
- os: ubuntu-latest
57+
node-version: "22" # minimum version on primary platform
58+
- os: ubuntu-latest
59+
node-version: "24" # latest on primary platform
60+
- os: macos-latest
61+
node-version: "24" # latest on macOS
62+
- os: windows-latest
63+
node-version: "24" # latest on Windows
64+
steps:
65+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
66+
67+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
68+
with:
69+
node-version: ${{ matrix.node-version }}
70+
cache: "npm"
71+
72+
- run: npm ci
73+
74+
# Unit suite (unit tests + snapshot tests + property-based tests)
75+
- name: Unit tests
76+
run: npm test
77+
78+
# E2E tests — process-isolated child-process harness (stdin/stdout, no PTY).
79+
# Verified cross-platform: runs on Linux, macOS, and Windows.
80+
# See https://github.com/agenticoding/pi-agenticoding/issues/12
81+
- name: E2E tests
82+
run: npm run test:e2e
83+
84+
# Upload test results for debugging — artifacts available for 30 days.
85+
- name: Upload test results
86+
if: always()
87+
uses: actions/upload-artifact@v4
88+
with:
89+
name: test-results-${{ matrix.os }}-node-${{ matrix.node-version }}
90+
path: |
91+
test-results/
92+
tests/__snapshots__/
93+
retention-days: 30

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ Before submitting, check that your change:
4646
- `npm run test:snapshots:update` — rewrites the golden files in `tests/__snapshots__/` after an intentional render change. Review the diff carefully: snapshot updates are the only signal that catches unintended UI regressions.
4747
- `npm run test:e2e` — runs the process-isolated end-to-end suite under `tests/e2e/`.
4848

49+
## CI
50+
51+
Pull requests are automatically tested via GitHub Actions. The pipeline runs:
52+
53+
1. **Quick-check** (Ubuntu, Node 22): `npm ci`, type check (`npx tsc --noEmit`), and security audit (`npm audit`). Catches trivial failures before the full matrix.
54+
2. **Cross-platform matrix** (depends on quick-check): Unit tests on Ubuntu (Node 22 + 24), macOS (Node 24), and Windows (Node 24). E2E tests on all platforms.
55+
56+
Snapshot golden files in `tests/__snapshots__/` are stored with LF line endings (enforced by `.gitattributes`). The `normalizeEOL` helper in the snapshot test file normalizes `\r\n` to `\n` on read, so Windows developers get correct comparisons even if their working tree has CRLF. If you update snapshots, the CI matrix validates them on all platforms.
57+
The E2E suite runs on all platforms including Windows (verified in issue #12).
58+
4959
## Community
5060

5161
Use GitHub Issues for bug reports and feature requests. Keep discussions concrete: describe the agent workflow you expected, what happened instead, and any reproduction steps.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"type": "git",
1212
"url": "git+https://github.com/agenticoding/pi-agenticoding.git"
1313
},
14+
"engines": {
15+
"node": ">=22"
16+
},
1417
"peerDependencies": {
1518
"@earendil-works/pi-ai": "*",
1619
"@earendil-works/pi-coding-agent": "*",

tests/e2e/pty-harness.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const ROOT = resolve(HERE, "..", "..");
1515
const LOADER = pathToFileURL(resolve(ROOT, "register-loader.mjs")).href;
1616

1717
export const DEFAULT_SCRIPT = resolve(HERE, "test-host.ts");
18+
const DEFAULT_TIMEOUT_MS = 5000;
19+
const TIMEOUT_MS = parseInt(process.env.E2E_TIMEOUT_MS ?? "", 10) || DEFAULT_TIMEOUT_MS;
1820

1921
export class PytestHarness {
2022
private child: ChildProcessWithoutNullStreams;
@@ -27,7 +29,7 @@ export class PytestHarness {
2729
scriptPath = DEFAULT_SCRIPT,
2830
options?: { timeoutMs?: number },
2931
) {
30-
this.timeoutMs = options?.timeoutMs ?? 5000;
32+
this.timeoutMs = options?.timeoutMs ?? TIMEOUT_MS;
3133

3234
const entry = isAbsolute(scriptPath) ? scriptPath : resolve(ROOT, scriptPath);
3335

tests/unit/render-snapshots.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,22 @@ function ensureSnapshotDir(): void {
5656
}
5757
}
5858

59+
/** Normalize line endings so golden files (stored with \n) match on Windows (\r\n). */
60+
function normalizeEOL(s: string): string {
61+
return s.replace(/\r?\n/g, "\n");
62+
}
63+
5964
function matchSnapshot(name: string, actual: string): void {
6065
ensureSnapshotDir();
6166
const file = join(SNAPSHOT_DIR, `${name}.txt`);
6267
if (process.env.UPDATE_SNAPSHOTS) {
63-
writeFileSync(file, actual);
68+
writeFileSync(file, normalizeEOL(actual));
6469
return;
6570
}
6671
if (!existsSync(file)) {
6772
assert.fail(`Snapshot ${name} is missing. Re-run with UPDATE_SNAPSHOTS=1 to create it.`);
6873
}
69-
const expected = readFileSync(file, "utf-8");
74+
const expected = normalizeEOL(readFileSync(file, "utf-8"));
7075
assert.equal(actual, expected, `Snapshot ${name} does not match`);
7176
}
7277

tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"module": "nodenext",
5+
"moduleResolution": "nodenext",
6+
"strict": true,
7+
"noEmit": true,
8+
"skipLibCheck": true,
9+
"verbatimModuleSyntax": true
10+
},
11+
"include": ["*.ts", "**/*.ts"]
12+
}

0 commit comments

Comments
 (0)