From 7714828e5ccf697baf4e55a0108d862370fd6aae Mon Sep 17 00:00:00 2001 From: flujo-app <300233937+flujo-app@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:12:25 -0500 Subject: [PATCH] Tests remove their temporary folders; CI fails if any are left Each focused test file now creates its folders through test/support/temp-dir.mjs, which removes them when that test process exits. scripts/test-temp-guard.mjs runs tests against a private temp location and fails on leftovers; CI uses it. Fixes #17 --- .github/workflows/public-sdk.yml | 5 +++-- scripts/test-temp-guard.mjs | 21 ++++++++++++++++++ test/cloud-lite-provider.test.mjs | 3 ++- test/cloud-lite.test.mjs | 3 ++- test/generic-swarm-evidence.test.mjs | 3 ++- test/model-provider.test.mjs | 3 ++- test/selfhost-modal.test.mjs | 3 ++- test/support/temp-dir.mjs | 22 +++++++++++++++++++ test/workflow-m0-lifecycle.test.mjs | 5 ++--- test/workflow-m1-portable-artifacts.test.mjs | 23 ++++++++++---------- test/workflow-m2-ack-recovery.test.mjs | 3 ++- test/workflow-m2-authority-recovery.test.mjs | 3 ++- test/workflow-native-wake.test.mjs | 3 ++- test/workflows.test.mjs | 6 ++--- 14 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 scripts/test-temp-guard.mjs create mode 100644 test/support/temp-dir.mjs diff --git a/.github/workflows/public-sdk.yml b/.github/workflows/public-sdk.yml index 5ee5e8e..23af432 100644 --- a/.github/workflows/public-sdk.yml +++ b/.github/workflows/public-sdk.yml @@ -23,7 +23,8 @@ jobs: cache: npm - run: npm ci - run: npm audit --omit=dev --audit-level=high - - run: node --test test/cloud-lite.test.mjs test/cloud-lite-provider.test.mjs test/model-provider.test.mjs test/selfhost-modal.test.mjs test/public-sdk.test.mjs test/template-catalog.test.mjs test/factory-team-template.test.mjs test/flujo-workspace.test.mjs test/savia-case-plan.test.mjs test/savia-topology.test.mjs test/managed-fleet.test.mjs test/managed-fleet-real-module.test.mjs test/managed-fleet-provider-closure.test.mjs test/provider-cell-retirement.test.mjs test/presentation.test.mjs test/presentation-modal.test.mjs test/presentation-closure.test.mjs test/observatory-client.test.mjs test/workflows.test.mjs test/workflow-native-wake.test.mjs test/workflow-m0-lifecycle.test.mjs test/workflow-m1-portable-artifacts.test.mjs test/workflow-m2-ack-recovery.test.mjs test/workflow-m2-authority-recovery.test.mjs + - name: Focused tests, leaving nothing in the temp location + run: node scripts/test-temp-guard.mjs test/cloud-lite.test.mjs test/cloud-lite-provider.test.mjs test/model-provider.test.mjs test/selfhost-modal.test.mjs test/public-sdk.test.mjs test/template-catalog.test.mjs test/factory-team-template.test.mjs test/flujo-workspace.test.mjs test/savia-case-plan.test.mjs test/savia-topology.test.mjs test/managed-fleet.test.mjs test/managed-fleet-real-module.test.mjs test/managed-fleet-provider-closure.test.mjs test/provider-cell-retirement.test.mjs test/presentation.test.mjs test/presentation-modal.test.mjs test/presentation-closure.test.mjs test/observatory-client.test.mjs test/workflows.test.mjs test/workflow-native-wake.test.mjs test/workflow-m0-lifecycle.test.mjs test/workflow-m1-portable-artifacts.test.mjs test/workflow-m2-ack-recovery.test.mjs test/workflow-m2-authority-recovery.test.mjs - name: Self-hosted model settings (Python, offline) run: python3 -B -m unittest discover -s selfhost/modal -p "test_*.py" - name: Install and exercise the packed public package @@ -39,4 +40,4 @@ jobs: node "$RUNNER_TEMP/consumer/node_modules/flujo-factory/bin/factory-selfhost.mjs" profiles FACTORY_TEST_MCP="$RUNNER_TEMP/consumer/node_modules/flujo-factory/bin/factory-mcp.mjs" node --test --test-name-pattern="public MCP stdio" test/public-sdk.test.mjs - name: Generic workflow and evidence recovery regressions - run: node --test test/generic-swarm-evidence.test.mjs + run: node scripts/test-temp-guard.mjs test/generic-swarm-evidence.test.mjs diff --git a/scripts/test-temp-guard.mjs b/scripts/test-temp-guard.mjs new file mode 100644 index 0000000..1e24d94 --- /dev/null +++ b/scripts/test-temp-guard.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// Runs `node --test ` with the temp location pointed at a private empty +// folder, then fails if anything is left in it. Tests that leave folders behind +// would otherwise fill a developer's temp directory run after run. +import { mkdtempSync, readdirSync, rmSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const files = process.argv.slice(2); +if (files.length === 0) throw new Error('usage: test-temp-guard.mjs '); +const root = mkdtempSync(join(tmpdir(), 'factory-temp-guard-')); +const env = { ...process.env, TEMP: root, TMP: root, TMPDIR: root }; +delete env.FACTORY_TEST_TMP; +const result = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit', env }); +if (result.error) throw result.error; +const left = readdirSync(root).sort(); +console.log(`temp guard: ${left.length} item(s) left in the test temp location`); +for (const name of left.slice(0, 50)) console.log(` ${name}`); +rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); +process.exitCode = result.status || (left.length ? 1 : 0); diff --git a/test/cloud-lite-provider.test.mjs b/test/cloud-lite-provider.test.mjs index 4ca5c96..3e38652 100644 --- a/test/cloud-lite-provider.test.mjs +++ b/test/cloud-lite-provider.test.mjs @@ -7,8 +7,9 @@ import { validateCloudLitePlan, secret } from '../src/cloud-lite/protocol.mjs'; import { cloudLiteBootstrap } from '../src/cloud-lite/bootstrap.mjs'; import { FactoryCloudLiteFleet } from '../src/cloud-lite/fleet.mjs'; import { codexEnvironment, isStreamingDelta, nativeAgentConfig, providerConfigToml, runPrepareHook } from '../src/cloud-lite/runtime.mjs'; +import { tempDir } from './support/temp-dir.mjs'; -const fresh = () => mkdtempSync(join(tmpdir(), 'factory-cloud-lite-provider-')); +const fresh = () => tempDir('factory-cloud-lite-provider-'); // A self-hosted model with its real context window and an earlier compaction point. const provider = () => ({ id: 'selfhosted', name: 'Self-hosted model', envKey: 'SELF_HOSTED_MODEL_KEY', model: 'served-model', baseUrls: ['https://model-a.example.test/v1', 'https://model-b.example.test/v1'], contextWindow: 262144, autoCompactTokenLimit: 80000 }); diff --git a/test/cloud-lite.test.mjs b/test/cloud-lite.test.mjs index adebfed..1e96ed7 100644 --- a/test/cloud-lite.test.mjs +++ b/test/cloud-lite.test.mjs @@ -13,9 +13,10 @@ import { NativeOperatorStop } from '../src/cloud-lite/operator-stop.mjs'; import { collectCloudLiteSnapshot } from '../src/cloud-lite/collect.mjs'; import { finalizeNativeStopCommand, isCurrentNativeTurn, nativeStopTurn } from '../src/cloud-lite/runtime.mjs'; import { DatabaseSync } from 'node:sqlite'; +import { tempDir } from './support/temp-dir.mjs'; const plan = () => validateCloudLitePlan({ runId: 'lite-test', app: 'factory-lite-test', repository: 'mario-andreschak/FLUJO', goal: 'Analyze and merge existing PRs', machines: 10, agentsPerMachine: 10 }); -const fresh = () => mkdtempSync(join(tmpdir(), 'factory-cloud-lite-')); +const fresh = () => tempDir('factory-cloud-lite-'); test('requested 10 by 10 topology and Fly resource selection retain exact values', () => { const input = plan(); const agents = agentInventory(input); assert.equal(agents.length, 100); assert.equal(agents.filter(x => x.role === 'orchestrator').length, 10); diff --git a/test/generic-swarm-evidence.test.mjs b/test/generic-swarm-evidence.test.mjs index 22ad20e..3ca0876 100644 --- a/test/generic-swarm-evidence.test.mjs +++ b/test/generic-swarm-evidence.test.mjs @@ -14,9 +14,10 @@ import { runVerification } from '../src/workflows/verifier.mjs'; import { ContinuousEvidenceCollector, createEvidenceClient, startEvidenceServer } from '../src/evidence.mjs'; import { FactoryCloudLiteFleet } from '../src/cloud-lite/fleet.mjs'; import { validateCloudLitePlan } from '../src/cloud-lite/protocol.mjs'; +import { tempDir } from './support/temp-dir.mjs'; // The invoking release harness owns cleanup through FLUJO Bash MCP; retain failure evidence. -function fixture(){const parent=process.env.FACTORY_TEST_TMP??tmpdir();mkdirSync(parent,{recursive:true});const root=mkdtempSync(join(parent,'factory-generic-'));const source=join(root,'source');mkdirSync(source);return{root,source};} +function fixture(){const root=tempDir('factory-generic-');const source=join(root,'source');mkdirSync(source);return{root,source};} function workflowFixture(){ const f=fixture(),database=join(f.root,'control.sqlite'),control=new FactoryControl(database); control.initializeSwarm({mission:'Research a question',budgetCents:0,maxCells:6,maxDepth:2},[ diff --git a/test/model-provider.test.mjs b/test/model-provider.test.mjs index 2054156..667ccf3 100644 --- a/test/model-provider.test.mjs +++ b/test/model-provider.test.mjs @@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'; import { claudeProviderEnvironment, codexProviderConfig, codexProviderOverrides, liteWorkerCommand, providerPrograms, validateModelProvider } from '../src/public-sdk.mjs'; import { validateCloudLitePlan, secret } from '../src/cloud-lite/protocol.mjs'; import { providerConfigToml } from '../src/cloud-lite/runtime.mjs'; +import { tempDir } from './support/temp-dir.mjs'; // One description of a self-hosted vLLM endpoint that serves every wire API. const endpoint = (extra = {}) => ({ id: 'selfhosted', name: 'Self-hosted model', model: 'served-model', envKey: 'SELF_HOSTED_MODEL_KEY', @@ -74,7 +75,7 @@ test('local lite workers launch Codex and Claude Code against a self-hosted mode }); test('factory-lite reads an endpoint description and refuses to start without its key', () => { - const dir = mkdtempSync(join(tmpdir(), 'factory-lite-provider-')); + const dir = tempDir('factory-lite-provider-'); const file = join(dir, 'endpoint.json'); writeFileSync(file, JSON.stringify(endpoint())); const cli = fileURLToPath(new URL('../bin/factory-lite.mjs', import.meta.url)); const env = { ...process.env }; delete env.SELF_HOSTED_MODEL_KEY; diff --git a/test/selfhost-modal.test.mjs b/test/selfhost-modal.test.mjs index 8d99e93..7ab7b8e 100644 --- a/test/selfhost-modal.test.mjs +++ b/test/selfhost-modal.test.mjs @@ -6,8 +6,9 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { ModalSelfHostedModel, selfHostedProfiles, codexProviderConfig, claudeProviderEnvironment, validateCloudLitePlan } from '../src/public-sdk.mjs'; +import { tempDir } from './support/temp-dir.mjs'; -const fresh = () => mkdtempSync(join(tmpdir(), 'factory-selfhost-')); +const fresh = () => tempDir('factory-selfhost-'); const read = (path) => readFileSync(path, 'utf8'); // A fake Modal account: the deploy script, the Modal CLI and the served model. diff --git a/test/support/temp-dir.mjs b/test/support/temp-dir.mjs new file mode 100644 index 0000000..f8b7f3b --- /dev/null +++ b/test/support/temp-dir.mjs @@ -0,0 +1,22 @@ +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +// Every test file runs in its own process, so that process owns its temporary +// folders and removes them when it exits, whichever way its tests ended. +const owned = new Set(); +process.once('exit', () => { + // Windows can hold a file briefly after a child process or database closes. + for (const directory of owned) { + try { rmSync(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch { /* The temp guard reports leftovers. */ } + } +}); + +/** A new temporary folder named `XXXXXX`, removed when this test process exits. */ +export function tempDir(prefix) { + const parent = resolve(process.env.FACTORY_TEST_TMP ?? tmpdir()); + mkdirSync(parent, { recursive: true }); + const directory = mkdtempSync(join(parent, prefix)); + owned.add(directory); + return directory; +} diff --git a/test/workflow-m0-lifecycle.test.mjs b/test/workflow-m0-lifecycle.test.mjs index 8fd2a60..80330d6 100644 --- a/test/workflow-m0-lifecycle.test.mjs +++ b/test/workflow-m0-lifecycle.test.mjs @@ -8,13 +8,12 @@ import { fileURLToPath } from 'node:url'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { Factory, FactoryWorkflow, captureGitSource, contentDigest, runVerification } from '../src/public-sdk.mjs'; +import { tempDir } from './support/temp-dir.mjs'; const mcpServer = fileURLToPath(new URL('../bin/factory-mcp.mjs', import.meta.url)); function fixture({ ignoredGateInput = false } = {}) { - const parent = resolve(process.env.FACTORY_TEST_TMP ?? tmpdir()); - mkdirSync(parent, { recursive: true }); - const root = mkdtempSync(join(parent, 'factory-workflow-m0-')); + const root = tempDir('factory-workflow-m0-'); const cwd = join(root, 'source'); mkdirSync(cwd); const git = (...args) => execFileSync('git', args, { cwd, encoding: 'utf8', windowsHide: true }).trim(); git('init'); git('config', 'user.email', 'workflow-test@example.invalid'); git('config', 'user.name', 'Workflow test'); diff --git a/test/workflow-m1-portable-artifacts.test.mjs b/test/workflow-m1-portable-artifacts.test.mjs index 4322e5c..c7f56e5 100644 --- a/test/workflow-m1-portable-artifacts.test.mjs +++ b/test/workflow-m1-portable-artifacts.test.mjs @@ -8,11 +8,12 @@ import { createHash } from 'node:crypto'; import { DatabaseSync } from 'node:sqlite'; import { collectCloudLiteSnapshot } from '../src/cloud-lite/collect.mjs'; import { Factory, FactoryArtifactStore, FactoryWorkflow, captureGitSource, interpretVerification, reconcileVerification, runVerification, startVerification } from '../src/public-sdk.mjs'; +import { tempDir } from './support/temp-dir.mjs'; const sha256 = bytes => createHash('sha256').update(bytes).digest('hex'); test('SQLite snapshot backup supports deep workspace paths', async () => { - const root = mkdtempSync(join(tmpdir(), 'factory-deep-sqlite-')); + const root = tempDir('factory-deep-sqlite-'); const database = new DatabaseSync(toNamespacedPath(join(root, 'state.sqlite'))); database.exec("CREATE TABLE evidence(value TEXT); INSERT INTO evidence VALUES ('deep-workspace')"); database.close(); @@ -28,7 +29,7 @@ test('SQLite snapshot backup supports deep workspace paths', async () => { }); test('portable snapshot preserves and verifies real evidence while excluding private paths', async () => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-portable-')); + const root = tempDir('factory-workflow-m1-portable-'); mkdirSync(join(root, 'logs'), { recursive: true }); mkdirSync(join(root, 'cache', '.npm'), { recursive: true }); mkdirSync(join(root, 'safe-dir'), { recursive: true }); @@ -80,8 +81,8 @@ test('portable snapshot preserves and verifies real evidence while excluding pri }); test('portable snapshot retains internal directory links and omits links escaping its root', async t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-links-')); - const external = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-outside-')); + const root = tempDir('factory-workflow-m1-links-'); + const external = tempDir('factory-workflow-m1-outside-'); mkdirSync(join(root, 'safe-dir')); mkdirSync(join(external, 'outside-dir')); writeFileSync(join(root, 'safe-dir', 'inside.txt'), 'inside root'); @@ -128,7 +129,7 @@ test('portable snapshot retains internal directory links and omits links escapin }); test('public FactoryArtifactStore preserves content identity across relocation and rejects corruption and invalid ranges', t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-store-')); + const root = tempDir('factory-workflow-m1-store-'); const directory = join(root, 'store'); const moved = join(root, 'relocated-store'); const store = new FactoryArtifactStore(directory); @@ -208,7 +209,7 @@ function verifiedFixture(root, { failingGate = false, environmentCanary = false, } test('candidate submission carries the captured archive and verifier binds execution to it', async t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-source-')); + const root = tempDir('factory-workflow-m1-source-'); const f = verifiedFixture(root); t.after(() => f.workflow.close()); assert.equal(f.candidate.source.commit, f.source.commit); @@ -236,7 +237,7 @@ test('node-tap adapter rejects an exit-zero run with only skipped tests', () => }); test('verification refuses a submitted archive that differs from the gate source', t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-mismatch-')); + const root = tempDir('factory-workflow-m1-mismatch-'); const f = verifiedFixture(root, { mismatchArchive: true }); t.after(() => f.workflow.close()); assert.throws(() => startVerification(f.workflow, { runId: 'm1-source-mismatch', candidateId: f.candidate.id, @@ -244,7 +245,7 @@ test('verification refuses a submitted archive that differs from the gate source }); test('ignored host overlay absent from captured archive cannot make a gate pass', async t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-ignored-')); + const root = tempDir('factory-workflow-m1-ignored-'); const f = verifiedFixture(root, { ignoredGate: true }); t.after(() => f.workflow.close()); writeFileSync(join(f.cwd, '.git', 'info', 'exclude'), 'check.mjs\n'); @@ -258,7 +259,7 @@ test('ignored host overlay absent from captured archive cannot make a gate pass' }); test('empty gate sets require an explicit review-only policy', t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-review-only-')); + const root = tempDir('factory-workflow-m1-review-only-'); const f = verifiedFixture(root); t.after(() => f.workflow.close()); const input = { taskId: 'review-only-m1', projectId: 'project-m1', branch: 'codex/review-only', problem: 'Review evidence', @@ -271,7 +272,7 @@ test('runner drops inherited canary environment and diagnostics retain no secret const secret = Buffer.from('synthetic bootstrap secret').toString('base64'); const prior = process.env.FACTORY_M1_CANARY; process.env.FACTORY_M1_CANARY = secret; - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-env-')); + const root = tempDir('factory-workflow-m1-env-'); const f = verifiedFixture(root, { environmentCanary: true }); t.after(() => { f.workflow.close(); if (prior === undefined) delete process.env.FACTORY_M1_CANARY; else process.env.FACTORY_M1_CANARY = prior; }); const result = await runVerification(f.workflow, { runId: 'm1-env-isolation', candidateId: f.candidate.id, gateId: f.gate.id, cwd: f.cwd }); @@ -282,7 +283,7 @@ test('runner drops inherited canary environment and diagnostics retain no secret }); test('runner receipt replay rejects a fabricated pass for an actually failing candidate', async t => { - const root = mkdtempSync(join(tmpdir(), 'factory-workflow-m1-receipt-')); + const root = tempDir('factory-workflow-m1-receipt-'); const f = verifiedFixture(root, { failingGate: true }); t.after(() => f.workflow.close()); const runId = 'm1-real-run'; diff --git a/test/workflow-m2-ack-recovery.test.mjs b/test/workflow-m2-ack-recovery.test.mjs index eb4e2c9..94915da 100644 --- a/test/workflow-m2-ack-recovery.test.mjs +++ b/test/workflow-m2-ack-recovery.test.mjs @@ -6,8 +6,9 @@ import { join } from 'node:path'; import { CloudLiteTrace } from '../src/cloud-lite/trace.mjs'; import { startCloudLiteCoordinator } from '../src/cloud-lite/server.mjs'; import { validateCloudLitePlan, agentInventory, secret } from '../src/cloud-lite/protocol.mjs'; +import { tempDir } from './support/temp-dir.mjs'; -const fresh = () => mkdtempSync(join(tmpdir(), 'factory-workflow-m2-ack-')); +const fresh = () => tempDir('factory-workflow-m2-ack-'); const event = seq => ({ seq, kind: `durable-${seq}`, body: { payload: `event-${seq}` }, observedAt: '2026-10-15T00:00:00.000Z' }); test('recovery does not trust a local ack that is ahead of the coordinator', async () => { diff --git a/test/workflow-m2-authority-recovery.test.mjs b/test/workflow-m2-authority-recovery.test.mjs index 6a2cecd..bd51424 100644 --- a/test/workflow-m2-authority-recovery.test.mjs +++ b/test/workflow-m2-authority-recovery.test.mjs @@ -8,8 +8,9 @@ import { createWorkflowClient, createWorkflowComparison, launchWorkflowCompariso import { contentDigest } from '../src/workflows/artifacts.mjs'; import { startCloudLiteCoordinator } from '../src/cloud-lite/server.mjs'; import { agentInventory, secret } from '../src/cloud-lite/protocol.mjs'; +import { tempDir } from './support/temp-dir.mjs'; -const fresh = () => mkdtempSync(join(tmpdir(), 'factory-workflow-m2-authority-')); +const fresh = () => tempDir('factory-workflow-m2-authority-'); function sourceFixture(root) { const cwd = join(root, 'source'); mkdirSync(cwd); diff --git a/test/workflow-native-wake.test.mjs b/test/workflow-native-wake.test.mjs index 6183183..ef94894 100644 --- a/test/workflow-native-wake.test.mjs +++ b/test/workflow-native-wake.test.mjs @@ -4,8 +4,9 @@ import {mkdtempSync,mkdirSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {NativeWorkflowWake} from '../src/cloud-lite/workflow-wake.mjs'; +import { tempDir } from './support/temp-dir.mjs'; const threadId='00000000-0000-0000-0000-000000000001'; -function file(){const parent=process.env.FACTORY_TEST_TMP??tmpdir();mkdirSync(parent,{recursive:true});return join(mkdtempSync(join(parent,'factory-wake-')),'wake.json');} +function file(){return join(tempDir('factory-wake-'),'wake.json');} test('native wake retains a rejected start and retries only after native idle readback',async()=>{ let starts=0;const rpc={request:async method=>{if(method==='thread/read')return {thread:{id:threadId,status:{type:'idle'},turns:[{id:'before'}]}}; if(++starts===1)throw Object.assign(new Error('Invalid request'),{code:-32602});return {turn:{id:'after'}};}}; diff --git a/test/workflows.test.mjs b/test/workflows.test.mjs index 44cd647..59c90ed 100644 --- a/test/workflows.test.mjs +++ b/test/workflows.test.mjs @@ -7,11 +7,11 @@ import { execFileSync } from 'node:child_process'; import { Factory, FactoryWorkflow, FactoryWorkflowSupervisor, runVerification, createWorkflowClient, createWorkflowComparison, summarizeWorkflowComparison, captureGitSource } from '../src/public-sdk.mjs'; import { startCloudLiteCoordinator } from '../src/cloud-lite/server.mjs'; import { assertCheckoutSource } from '../src/workflows/source.mjs'; +import { tempDir } from './support/temp-dir.mjs'; // Keep generated evidence for the invoking harness; cleanup is performed through FLUJO Bash MCP. function fixture() { - const parent = process.env.FACTORY_TEST_TMP ?? tmpdir(); mkdirSync(parent, { recursive: true }); - const root = mkdtempSync(join(parent, 'factory-workflow-')), cwd = join(root,'source'); mkdirSync(cwd); + const root = tempDir('factory-workflow-'), cwd = join(root,'source'); mkdirSync(cwd); const git = (...args) => execFileSync('git', args, { cwd, encoding:'utf8', windowsHide: true }).trim(); git('init'); git('config','user.email','workflow-test@example.invalid'); git('config','user.name','Workflow test'); writeFileSync(join(cwd,'check.mjs'), "console.log('TAP version 13\\nok 1 - actual behavior\\n1..1\\n# tests 1\\n# pass 1\\n# fail 0\\n# cancelled 0');\n"); @@ -52,7 +52,7 @@ test('portable source rejects export omissions and substitutions and preserves l }); test('interrupted artifact writes cannot publish or poison a content reference', () => { - const directory=mkdtempSync(join(process.env.FACTORY_TEST_TMP??tmpdir(),'factory-artifact-fault-')); + const directory=tempDir('factory-artifact-fault-'); const module=new URL('../src/workflows/artifacts.mjs',import.meta.url).href; execFileSync(process.execPath,['--input-type=module','-e',` import fs from 'node:fs'; import assert from 'node:assert/strict';