Skip to content

Commit bfc9e15

Browse files
committed
ci(test): run toolkit package tests + fn init e2e on every PR
Wave 7 of the portable-functions toolkit. The existing test workflow ran pnpm test:unit and pnpm test:integration only, which skipped the package-local jest suites (fn-generator's snapshot, fn-client's API tests, fn-cli's init tests). That gap is now closed. Two new jobs in .github/workflows/test.yaml: 1. **toolkit** — installs, builds all six fn-* packages in dependency order, then runs jest in fn-generator, fn-client, and fn-cli. This catches regressions in the toolkit code paths that the existing tests:unit/integration jobs don't reach. 2. **fn-init-e2e** — builds fn-cli, then runs the new binary integration suite at tests/integration/fn-init.test.ts. The suite spawns the compiled `dist/bin/fn.js` against a tmpdir and asserts: - node-graphql scaffolding produces handler.json + handler.ts - python scaffolding produces handler.py with type=python - duplicate scaffold without --force fails with exit 1 - --force overwrites and updates description - fn generate finds the just-scaffolded function This proves the bundled templates resolve correctly when fn is invoked from a non-package cwd — the load-bearing scenario for end-user installs. All 5 binary tests pass locally. Existing CI jobs (build/lint, unit, integration) untouched — these run in addition.
1 parent 720afaf commit bfc9e15

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

.github/workflows/test.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,52 @@ jobs:
4040
- run: pnpm install
4141
- run: pnpm build
4242
- run: pnpm test:integration
43+
44+
toolkit:
45+
name: Toolkit package tests
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v5
49+
- uses: pnpm/action-setup@v6
50+
- uses: actions/setup-node@v5
51+
with:
52+
node-version: '22'
53+
cache: 'pnpm'
54+
- run: node --experimental-strip-types scripts/generate.ts
55+
- run: pnpm install
56+
- name: Build toolkit packages
57+
run: |
58+
pnpm --filter @constructive-io/fn-types build
59+
pnpm --filter @constructive-io/knative-job-fn build
60+
pnpm --filter @constructive-io/fn-runtime build
61+
pnpm --filter @constructive-io/fn-generator build
62+
pnpm --filter @constructive-io/fn-client build
63+
pnpm --filter @constructive-io/fn-cli build
64+
- name: Run toolkit unit tests
65+
run: |
66+
pnpm --filter @constructive-io/fn-generator test
67+
pnpm --filter @constructive-io/fn-client test
68+
pnpm --filter @constructive-io/fn-cli test
69+
70+
fn-init-e2e:
71+
name: fn init end-to-end
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@v5
75+
- uses: pnpm/action-setup@v6
76+
- uses: actions/setup-node@v5
77+
with:
78+
node-version: '22'
79+
cache: 'pnpm'
80+
- run: node --experimental-strip-types scripts/generate.ts
81+
- run: pnpm install
82+
- name: Build fn-cli (transitive)
83+
run: |
84+
pnpm --filter @constructive-io/fn-types build
85+
pnpm --filter @constructive-io/knative-job-fn build
86+
pnpm --filter @constructive-io/fn-runtime build
87+
pnpm --filter @constructive-io/fn-generator build
88+
pnpm --filter @constructive-io/fn-client build
89+
pnpm --filter @constructive-io/fn-cli build
90+
- name: Binary integration test
91+
run: pnpm exec jest tests/integration/fn-init.test.ts

tests/integration/fn-init.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { spawnSync } from 'child_process';
2+
import * as fs from 'fs';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
6+
const CLI_BIN = path.resolve(
7+
__dirname,
8+
'..',
9+
'..',
10+
'packages',
11+
'fn-cli',
12+
'dist',
13+
'bin',
14+
'fn.js'
15+
);
16+
17+
const runFn = (args: string[], cwd: string) =>
18+
spawnSync(process.execPath, [CLI_BIN, ...args], {
19+
cwd,
20+
env: { ...process.env, CI: 'true' },
21+
encoding: 'utf-8',
22+
});
23+
24+
describe('fn init (binary integration)', () => {
25+
let tmpRoot: string;
26+
27+
beforeAll(() => {
28+
if (!fs.existsSync(CLI_BIN)) {
29+
throw new Error(
30+
`fn-cli binary not built. Run \`pnpm --filter @constructive-io/fn-cli build\` first. Looked at ${CLI_BIN}.`
31+
);
32+
}
33+
});
34+
35+
beforeEach(() => {
36+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'fn-init-bin-'));
37+
});
38+
39+
afterEach(() => {
40+
fs.rmSync(tmpRoot, { recursive: true, force: true });
41+
});
42+
43+
it('scaffolds a node-graphql handler when invoked as a binary', () => {
44+
const result = runFn(['init', 'welcome', '--no-tty'], tmpRoot);
45+
expect(result.status).toBe(0);
46+
expect(result.stdout).toContain('Created welcome (node-graphql)');
47+
expect(
48+
fs.existsSync(path.join(tmpRoot, 'functions/welcome/handler.json'))
49+
).toBe(true);
50+
expect(
51+
fs.existsSync(path.join(tmpRoot, 'functions/welcome/handler.ts'))
52+
).toBe(true);
53+
});
54+
55+
it('scaffolds a python handler', () => {
56+
const result = runFn(
57+
['init', 'pybot', '--type', 'python', '--no-tty'],
58+
tmpRoot
59+
);
60+
expect(result.status).toBe(0);
61+
expect(
62+
fs.existsSync(path.join(tmpRoot, 'functions/pybot/handler.py'))
63+
).toBe(true);
64+
const json = JSON.parse(
65+
fs.readFileSync(
66+
path.join(tmpRoot, 'functions/pybot/handler.json'),
67+
'utf-8'
68+
)
69+
);
70+
expect(json.type).toBe('python');
71+
});
72+
73+
it('refuses to overwrite an existing function without --force', () => {
74+
expect(runFn(['init', 'a', '--no-tty'], tmpRoot).status).toBe(0);
75+
const second = runFn(['init', 'a', '--no-tty'], tmpRoot);
76+
expect(second.status).toBe(1);
77+
expect(second.stderr).toContain('already exists');
78+
});
79+
80+
it('overwrites with --force', () => {
81+
expect(runFn(['init', 'b', '--no-tty'], tmpRoot).status).toBe(0);
82+
const second = runFn(
83+
[
84+
'init',
85+
'b',
86+
'--no-tty',
87+
'--force',
88+
'--description',
89+
'overwritten',
90+
],
91+
tmpRoot
92+
);
93+
expect(second.status).toBe(0);
94+
const json = JSON.parse(
95+
fs.readFileSync(path.join(tmpRoot, 'functions/b/handler.json'), 'utf-8')
96+
);
97+
expect(json.description).toBe('overwritten');
98+
});
99+
100+
it('fn generate finds the just-scaffolded function', () => {
101+
runFn(['init', 'discoverme', '--no-tty'], tmpRoot);
102+
// fn generate needs a templates/ dir to do its full pipeline. For
103+
// this binary test we only verify discovery — the scaffolded
104+
// handler.json is enough for the scanner to enumerate it.
105+
fs.mkdirSync(path.join(tmpRoot, 'templates', 'node-graphql'), {
106+
recursive: true,
107+
});
108+
fs.mkdirSync(path.join(tmpRoot, 'templates', 'shared'), {
109+
recursive: true,
110+
});
111+
// Empty templates → generator runs but produces no per-fn files;
112+
// it still emits the manifest. Use --packages-only to skip k8s.
113+
const result = runFn(
114+
['generate', '--only', 'discoverme', '--packages-only'],
115+
tmpRoot
116+
);
117+
expect(result.status).toBe(0);
118+
});
119+
});

0 commit comments

Comments
 (0)