Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions test/gh-aw-deps-worker-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { afterAll, describe, expect, it } from 'vitest';
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, matchesGlob, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { extractSafeOutputsConfigJson } from './helpers/gh-aw-lock.js';
Expand Down Expand Up @@ -83,11 +82,17 @@ afterAll(() => {
* `create_pull_request` safe-output config gh-aw bakes into `.lock.yml`.
*/
function compileWorker(workflowId: string): Record<string, unknown> {
const workspace = mkdtempSync(resolve(tmpdir(), `${workflowId}-contract-`));
const workspace = mkdtempSync(resolve(ROOT, `.${workflowId}-contract-`));
compileWorkspaces.push(workspace);
const workflowDir = resolve(workspace, '.github', 'workflows');
mkdirSync(workflowDir, { recursive: true });
cpSync(resolve(ROOT, 'workflows'), workflowDir, { recursive: true });
if (workflowId === 'squad-deps-worker') {
cpSync(
resolve(ROOT, 'workflows/package/squad-deps-worker.md'),
resolve(workflowDir, 'squad-deps-worker.md'),
);
}
execFileSync('git', ['init', '--quiet'], { cwd: workspace });
execFileSync(
'gh',
Expand Down Expand Up @@ -171,8 +176,8 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (
expect(excludeList.length, 'exclude list must contain exactly the Wave-1 manifest basenames').toBe(WAVE_1_MANIFEST_BASENAMES.length);
});

// ── T2: always-protected basenames absent from exclude, present in compiled ─
it('always-protected basenames are absent from authored exclude and present in compiled protected_files (T2)', () => {
// ── T2: always-protected basenames remain outside protected-files.exclude ──
it('always-protected basenames are absent from authored protected-files.exclude (T2)', () => {
const protectedFiles = yamlBlock(depsWorkerFrontmatter, 'protected-files');
const excludeList = listInBlock(protectedFiles, 'exclude');

Expand Down Expand Up @@ -215,10 +220,17 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (
).not.toContain(basename);
}

// T5: always-protected still in compiled protected_files
// T5: gh-aw catalog entries remain in compiled protected_files.
// CHANGELOG.md is not in gh-aw's built-in catalog, so the canonical
// workflow protects it structurally through compiled excluded_files.
for (const basename of ALWAYS_PROTECTED_BASENAMES) {
if (basename === 'CHANGELOG.md') continue;
expect(compiledProtectedFiles, `${basename} must remain in compiled protected_files`).toContain(basename);
}
expect(
compiledExcludedFiles,
'CHANGELOG.md must remain structurally protected in compiled excluded_files',
).toContain('CHANGELOG.md');
// .npmrc and .yarnrc.yml are protected structurally by absence from
// allowed-files -- gh-aw's built-in catalog does not include them in
// the compiled protected_files list, so we assert only that they are
Expand All @@ -233,7 +245,7 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (

// T8: vendored/generated paths in excluded_files
expect(compiledExcludedFiles).toEqual(
expect.arrayContaining(['node_modules/**', 'vendor/**', '.squad/**']),
expect.arrayContaining(['CHANGELOG.md', 'node_modules/**', 'vendor/**', '.squad/**']),
);
},
20000,
Expand Down Expand Up @@ -273,6 +285,7 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (

expect(excludedFiles).toEqual(
expect.arrayContaining([
'CHANGELOG.md',
'node_modules/**',
'**/node_modules/**',
'vendor/**',
Expand Down Expand Up @@ -461,6 +474,51 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (
);
});

it(
'detects: removing CHANGELOG.md protection from the live canonical source changes compiled output',
() => {
const workspace = mkdtempSync(resolve(ROOT, '.deps-worker-changelog-mutation-'));
compileWorkspaces.push(workspace);
const workflowDir = resolve(workspace, '.github', 'workflows');
mkdirSync(workflowDir, { recursive: true });
cpSync(resolve(ROOT, 'workflows'), workflowDir, { recursive: true });

const canonicalSource = read('workflows/package/squad-deps-worker.md');
const mutatedSource = canonicalSource.replace(/^\s+- CHANGELOG\.md\s*$/m, '');
expect(mutatedSource, 'mutation must remove CHANGELOG.md from the canonical source')
.not.toBe(canonicalSource);
writeFileSync(resolve(workflowDir, 'squad-deps-worker.md'), mutatedSource, 'utf-8');

execFileSync('git', ['init', '--quiet'], { cwd: workspace });
execFileSync(
'gh',
['aw', 'compile', 'squad-deps-worker', '--strict', '--no-check-update'],
{ cwd: workspace, encoding: 'utf8', stdio: 'pipe' },
);

const compiled = readFileSync(
resolve(workflowDir, 'squad-deps-worker.lock.yml'),
'utf8',
);
const jsonText = extractSafeOutputsConfigJson(compiled);
expect(jsonText, 'mutated dependency worker must compile a parseable safe-output config')
.toBeDefined();
const safeOutputs = JSON.parse(jsonText!) as Record<string, Record<string, unknown>>;
const compiledExcludedFiles = (
safeOutputs.create_pull_request?.excluded_files ?? []
) as string[];

expect(
() => expect(
compiledExcludedFiles,
'CHANGELOG.md must remain structurally protected in compiled excluded_files',
).toContain('CHANGELOG.md'),
'compiled regression assertion must fail after removing CHANGELOG.md from the canonical source',
).toThrow();
},
20000,
);

it(
'detects: Wave-1 exclusion leaked into general worker — compiled contract catches package.json in implement-worker exclude',
() => {
Expand All @@ -471,7 +529,7 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', (
// protected_files will drop it and this assertion throws -- proving the
// guard turns red on a genuine source mutation rather than a hand-built
// local array.
const workspace = mkdtempSync(resolve(tmpdir(), 'implement-worker-mutation-'));
const workspace = mkdtempSync(resolve(ROOT, '.implement-worker-mutation-'));
compileWorkspaces.push(workspace);
const workflowDir = resolve(workspace, '.github', 'workflows');
mkdirSync(workflowDir, { recursive: true });
Expand Down
1 change: 1 addition & 0 deletions workflows/package/squad-deps-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ safe-outputs:
- go.mod
- go.sum
excluded-files:
- CHANGELOG.md
- node_modules/**
- "**/node_modules/**"
- vendor/**
Expand Down
3 changes: 3 additions & 0 deletions workflows/squad-deps-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ safe-outputs:
- go.mod
- go.sum
excluded-files:
# Release history remains human-controlled even though gh-aw does not
# currently include CHANGELOG.md in its built-in protected-file catalog.
- "CHANGELOG.md"
# Never authorize vendored or generated dependency content, even once a
# manifest basename above is excluded from protection in a later slice.
# `excluded-files` strips these paths from the patch structurally, before
Expand Down
2 changes: 1 addition & 1 deletion workflows/squad-workflows.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"source": "workflows/package/squad-deps-worker.md",
"destination": ".github/workflows/squad-deps-worker.md",
"lock": ".github/workflows/squad-deps-worker.lock.yml",
"source_sha256": "e7234fb7e22ad648414f51c1ffa0647e16650c160c1cdbc15c734697e4615e62"
"source_sha256": "f800dcb2bf711aac318853fe0d562dbf1f0d8c458c255d4f9a2aea122cdcc47e"
},
{
"name": "squad-retro",
Expand Down
Loading