Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
503 lines
17 KiB
JavaScript
503 lines
17 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
|
|
function writeFixture({ name, bundle, testSource }) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `workflow-harness-${name}-`));
|
|
fs.writeFileSync(path.join(dir, 'bundle.json'), JSON.stringify(bundle, null, 2), 'utf8');
|
|
fs.writeFileSync(path.join(dir, 'test.cjs'), testSource, 'utf8');
|
|
return {
|
|
dir,
|
|
bundlePath: path.join(dir, 'bundle.json'),
|
|
testPath: path.join(dir, 'test.cjs')
|
|
};
|
|
}
|
|
|
|
function loadHarnessWithStubs(stubs) {
|
|
const harnessRoot = path.resolve(__dirname, '..');
|
|
const runPath = path.join(harnessRoot, 'run.cjs');
|
|
|
|
const depPaths = {
|
|
db: path.join(harnessRoot, 'lib', 'db.cjs'),
|
|
http: path.join(harnessRoot, 'lib', 'http.cjs'),
|
|
workflow: path.join(harnessRoot, 'lib', 'workflow.cjs'),
|
|
runs: path.join(harnessRoot, 'lib', 'runs.cjs'),
|
|
};
|
|
|
|
const saved = {};
|
|
for (const [key, p] of Object.entries(depPaths)) {
|
|
saved[p] = require.cache[p];
|
|
if (stubs[key]) {
|
|
require.cache[p] = { id: p, filename: p, loaded: true, exports: stubs[key] };
|
|
}
|
|
}
|
|
|
|
delete require.cache[runPath];
|
|
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
const mod = require(runPath);
|
|
|
|
return {
|
|
mod,
|
|
restore() {
|
|
delete require.cache[runPath];
|
|
for (const p of Object.values(depPaths)) {
|
|
if (saved[p]) require.cache[p] = saved[p];
|
|
else delete require.cache[p];
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
test('T004: imports bundle with --force and reports workflow id/key used', async () => {
|
|
const importCalls = [];
|
|
const { dir, bundlePath, testPath } = writeFixture({
|
|
name: 't004',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t004', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
module.exports = async (ctx) => {
|
|
ctx.expect.equal(ctx.workflow.key, 'fixture.t004', 'workflow key');
|
|
ctx.expect.equal(ctx.workflow.id, 'wf-123', 'workflow id');
|
|
};
|
|
`
|
|
});
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async ({ force }) => {
|
|
importCalls.push({ force });
|
|
return { createdWorkflows: [{ key: 'fixture.t004', workflowId: 'wf-123' }] };
|
|
},
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => {
|
|
throw new Error('waitForRun should not be called for this test');
|
|
},
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
const res = await runFixture({
|
|
testDir: dir,
|
|
bundlePath,
|
|
testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
});
|
|
assert.equal(res.state.workflowId, 'wf-123');
|
|
assert.equal(res.state.workflowKey, 'fixture.t004');
|
|
assert.deepEqual(importCalls, [{ force: true }]);
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|
|
|
|
test('T005: surfaces thrown error as FAIL and writes stack trace artifacts', async () => {
|
|
const { dir, bundlePath, testPath } = writeFixture({
|
|
name: 't005',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t005', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
module.exports = async () => {
|
|
throw new Error('boom');
|
|
};
|
|
`
|
|
});
|
|
|
|
test('T006: waitForRun timeout produces helpful diagnostic in artifacts', async () => {
|
|
const { dir, bundlePath, testPath } = writeFixture({
|
|
name: 't006',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t006', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
module.exports = async (ctx) => {
|
|
await ctx.waitForRun({ startedAfter: ctx.triggerStartedAt, timeoutMs: 5 });
|
|
};
|
|
`
|
|
});
|
|
|
|
const timeoutError = new Error('Timed out waiting for workflow run');
|
|
timeoutError.details = { lastSeen: null, recentRuns: [] };
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t006', workflowId: 'wf-006' }] }),
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => {
|
|
throw timeoutError;
|
|
},
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
await assert.rejects(
|
|
() =>
|
|
runFixture({
|
|
testDir: dir,
|
|
bundlePath,
|
|
testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
}),
|
|
(err) => {
|
|
assert.ok(err.artifactsDir, 'expected err.artifactsDir to be set');
|
|
const ctxPath = path.join(err.artifactsDir, 'failure.context.json');
|
|
const parsed = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
assert.equal(parsed.error.message, 'Timed out waiting for workflow run');
|
|
assert.deepEqual(parsed.error.details, { lastSeen: null, recentRuns: [] });
|
|
return true;
|
|
}
|
|
);
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|
|
|
|
test('T007: captures run and step status summary on success', async () => {
|
|
const { dir, bundlePath, testPath } = writeFixture({
|
|
name: 't007',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t007', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
module.exports = async (ctx) => {
|
|
const run = await ctx.waitForRun({ startedAfter: ctx.triggerStartedAt, timeoutMs: 5 });
|
|
ctx.expect.equal(run.status, 'SUCCEEDED', 'run status');
|
|
};
|
|
`
|
|
});
|
|
|
|
const steps = [
|
|
{ step_id: 's1', run_id: 'run-007', step_path: '/0', definition_step_id: 'a', status: 'SUCCEEDED', attempt: 1 }
|
|
];
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t007', workflowId: 'wf-007' }] }),
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => ({ run_id: 'run-007', status: 'SUCCEEDED' }),
|
|
getRunSteps: async () => steps,
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: (s) => ({ counts: { SUCCEEDED: s.length }, failed: [] })
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
const res = await runFixture({
|
|
testDir: dir,
|
|
bundlePath,
|
|
testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
});
|
|
assert.deepEqual(res.state.run, { run_id: 'run-007', status: 'SUCCEEDED' });
|
|
assert.deepEqual(res.state.steps, steps);
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|
|
|
|
test('T008: prints single-line PASS/FAIL summary and correct exit code', async () => {
|
|
const { dir } = writeFixture({
|
|
name: 't008',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t008', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `module.exports = async () => {};`
|
|
});
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t008', workflowId: 'wf-008' }] }),
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => ({ run_id: 'run-008', status: 'SUCCEEDED' }),
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { runCliOnceForTests } = harness.mod;
|
|
const res = await runCliOnceForTests([
|
|
'--test',
|
|
dir,
|
|
'--base-url',
|
|
'http://localhost:3010',
|
|
'--tenant',
|
|
'tenant',
|
|
'--cookie',
|
|
'cookie',
|
|
'--force',
|
|
'--timeout-ms',
|
|
'1000'
|
|
]);
|
|
assert.equal(res.exitCode, 0);
|
|
assert.match(res.stdout[0], /^PASS workflow-harness-t008-/);
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|
|
|
|
test('T010: supports --debug to print verbose logs', async () => {
|
|
const { dir, bundlePath, testPath } = writeFixture({
|
|
name: 't010',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t010', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `module.exports = async () => {};`
|
|
});
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t010', workflowId: 'wf-010' }] }),
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => ({ run_id: 'run-010', status: 'SUCCEEDED' }),
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
});
|
|
|
|
const captured = [];
|
|
const orig = console.error;
|
|
console.error = (...args) => captured.push(args.join(' '));
|
|
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
await runFixture({
|
|
testDir: dir,
|
|
bundlePath,
|
|
testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: true,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
});
|
|
} finally {
|
|
console.error = orig;
|
|
harness.restore();
|
|
}
|
|
|
|
assert.ok(captured.some((l) => l.includes('[harness] importSummary')), 'expected importSummary debug log');
|
|
assert.ok(captured.some((l) => l.includes('[harness] workflow')), 'expected workflow debug log');
|
|
});
|
|
|
|
test('T011: runs registered cleanup hooks on PASS and on FAIL', async () => {
|
|
const cleanupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-harness-cleanup-'));
|
|
const passMarker = path.join(cleanupDir, 'pass.txt');
|
|
const failMarker = path.join(cleanupDir, 'fail.txt');
|
|
|
|
const passFixture = writeFixture({
|
|
name: 't011-pass',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t011', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
const fs = require('node:fs');
|
|
module.exports = async (ctx) => {
|
|
ctx.onCleanup(async () => fs.writeFileSync(${JSON.stringify(passMarker)}, 'ok', 'utf8'));
|
|
};
|
|
`
|
|
});
|
|
|
|
const failFixture = writeFixture({
|
|
name: 't011-fail',
|
|
bundle: {
|
|
format: 'alga-psa.workflow-bundle',
|
|
formatVersion: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
workflows: [{ key: 'fixture.t011', metadata: {}, dependencies: { actions: [], nodeTypes: [], schemaRefs: [] }, draft: { draftVersion: 1, definition: {} }, publishedVersions: [] }]
|
|
},
|
|
testSource: `
|
|
const fs = require('node:fs');
|
|
module.exports = async (ctx) => {
|
|
ctx.onCleanup(async () => fs.writeFileSync(${JSON.stringify(failMarker)}, 'ok', 'utf8'));
|
|
throw new Error('boom');
|
|
};
|
|
`
|
|
});
|
|
|
|
const baseStubs = {
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t011', workflowId: 'wf-011' }] }),
|
|
exportWorkflowBundleV1: async () => ({})
|
|
},
|
|
runs: {
|
|
waitForRun: async () => ({ run_id: 'run-011', status: 'SUCCEEDED' }),
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
};
|
|
|
|
const harness = loadHarnessWithStubs(baseStubs);
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
|
|
await runFixture({
|
|
testDir: passFixture.dir,
|
|
bundlePath: passFixture.bundlePath,
|
|
testPath: passFixture.testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
});
|
|
assert.ok(fs.existsSync(passMarker), 'expected pass cleanup marker to be written');
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
runFixture({
|
|
testDir: failFixture.dir,
|
|
bundlePath: failFixture.bundlePath,
|
|
testPath: failFixture.testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
}),
|
|
/boom/
|
|
);
|
|
assert.ok(fs.existsSync(failMarker), 'expected fail cleanup marker to be written');
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|
|
|
|
const harness = loadHarnessWithStubs({
|
|
http: { createHttpClient: () => ({ request: async () => ({ json: {} }) }) },
|
|
db: { createDbClient: async () => ({ query: async () => [], close: async () => {} }) },
|
|
workflow: {
|
|
importWorkflowBundleV1: async () => ({ createdWorkflows: [{ key: 'fixture.t005', workflowId: 'wf-005' }] }),
|
|
exportWorkflowBundleV1: async () => ({ exported: true })
|
|
},
|
|
runs: {
|
|
waitForRun: async () => {
|
|
throw new Error('waitForRun should not be called for this test');
|
|
},
|
|
getRunSteps: async () => [],
|
|
getRunLogs: async () => [],
|
|
summarizeSteps: () => ({ counts: {}, failed: [] })
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { runFixture } = harness.mod;
|
|
await assert.rejects(
|
|
() =>
|
|
runFixture({
|
|
testDir: dir,
|
|
bundlePath,
|
|
testPath,
|
|
baseUrl: 'http://localhost:3010',
|
|
tenantId: 'tenant',
|
|
cookie: 'cookie',
|
|
force: true,
|
|
timeoutMs: 1000,
|
|
debug: false,
|
|
artifactsDir: os.tmpdir(),
|
|
pgUrl: 'postgres://unused'
|
|
}),
|
|
(err) => {
|
|
assert.match(String(err.message), /boom/);
|
|
assert.ok(err.artifactsDir, 'expected err.artifactsDir to be set');
|
|
const ctxPath = path.join(err.artifactsDir, 'failure.context.json');
|
|
const errPath = path.join(err.artifactsDir, 'failure.error.txt');
|
|
assert.ok(fs.existsSync(ctxPath), 'expected failure.context.json');
|
|
assert.ok(fs.existsSync(errPath), 'expected failure.error.txt');
|
|
assert.match(fs.readFileSync(errPath, 'utf8'), /boom/);
|
|
return true;
|
|
}
|
|
);
|
|
} finally {
|
|
harness.restore();
|
|
}
|
|
});
|