Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8934b71
feat(rechallenge): add types and error classes
rinatkhaziev Jun 4, 2026
8655df4
feat(rechallenge): add keychain-backed per-scope elevated-token cache
rinatkhaziev Jun 4, 2026
35f9916
feat(rechallenge): add REST client for Parker v2 session endpoints
rinatkhaziev Jun 4, 2026
a6df809
fix(rechallenge): satisfy tsc check-types in test files
rinatkhaziev Jun 4, 2026
b2c46de
feat(rechallenge): orchestrate session create, poll, exchange
rinatkhaziev Jun 4, 2026
3e6cc2f
feat(rechallenge): add Apollo link that intercepts elevated-permissio…
rinatkhaziev Jun 4, 2026
f03c423
feat(api): insert rechallenge link into Apollo chain
rinatkhaziev Jun 4, 2026
a461f52
feat(defensive-mode): add GraphQL helpers for status and config mutat…
rinatkhaziev Jun 4, 2026
b8815af
feat(cli): add vip defensive-mode parent command
rinatkhaziev Jun 4, 2026
20e4e0f
feat(cli): add vip defensive-mode enable subcommand
rinatkhaziev Jun 4, 2026
2dc5804
feat(cli): add vip defensive-mode disable subcommand
rinatkhaziev Jun 4, 2026
1109b2c
feat(cli): add vip defensive-mode configure subcommand
rinatkhaziev Jun 4, 2026
c700b4e
feat(cli): register vip defensive-mode top-level command
rinatkhaziev Jun 4, 2026
80e0d1d
feat(logout): clear elevated-token cache on logout
rinatkhaziev Jun 4, 2026
6763ec4
fix(api): lint cleanup for lazy rechallenge link import
rinatkhaziev Jun 4, 2026
a353924
fix: address final-review findings (diff in configure, non-interactiv…
rinatkhaziev Jun 4, 2026
c9fc8df
fix: address PR review feedback (data guards, version constant, paylo…
rinatkhaziev Jun 4, 2026
5a7ca83
fix: address PR review feedback (output formatting, login hardening, …
rinatkhaziev Jun 10, 2026
8736474
chore(lint): scope no-await-in-loop disable to rechallenge polling loop
rinatkhaziev Jun 10, 2026
3a29646
fix(lint): stop importing Response type from node-fetch in rechalleng…
rinatkhaziev Jun 10, 2026
2750aa4
Merge branch 'trunk' into feat/rechallenge-v2-defensive-mode
rinatkhaziev Jun 10, 2026
758fda5
feat(edge-workers): add edge workers commands
alexcriss Jun 10, 2026
13734c6
Merge branch 'trunk' into add/edge-workers-commands
rinatkhaziev Jul 6, 2026
c59fde2
Bump the SDK version to get the new resp.request functionality
alexcriss Jul 15, 2026
029b16c
Merge branch 'trunk' into add/edge-workers-commands
rinatkhaziev Aug 19, 2026
a3e087c
fix(edge-workers): contain project filesystem paths
rinatkhaziev Aug 19, 2026
2bc1165
fix(edge-workers): refuse destructive project initialization
rinatkhaziev Aug 19, 2026
035a32f
fix(edge-workers): fail closed on API results
rinatkhaziev Aug 19, 2026
4493a8f
feat(edge-workers): preview deployments before apply
rinatkhaziev Aug 19, 2026
edc8022
fix(edge-workers): store empty deployment source
rinatkhaziev Aug 19, 2026
6d19c81
fix(edge-workers): guard production mutations
rinatkhaziev Aug 19, 2026
42c557c
fix(api): minimize edge-worker and debug data
rinatkhaziev Aug 19, 2026
b057b59
docs(edge-workers): ship safe scaffold guidance
rinatkhaziev Aug 19, 2026
305128f
fix(edge-workers): address scaffold review gaps
rinatkhaziev Aug 19, 2026
b118e52
test(edge-workers): complete lifecycle coverage
rinatkhaziev Aug 19, 2026
edaebb7
test(edge-workers): assert single terminal exits
rinatkhaziev Aug 19, 2026
45a25a7
fix(edge-workers): close final hardening gaps
rinatkhaziev Aug 19, 2026
9079c5f
feat(edge-workers): support enable after deploy
rinatkhaziev Aug 19, 2026
1a1c9d2
feat(edge-workers): add explicit deploy enable flow
rinatkhaziev Aug 19, 2026
7ea4cd8
fix(edge-workers): address review feedback
rinatkhaziev Aug 20, 2026
daed446
ci: ensure Docker runs on Windows tests
rinatkhaziev Aug 20, 2026
6d353b3
build(edge-workers): bump SDK to 0.3.2
rinatkhaziev Aug 20, 2026
a0da7b4
fix(edge-workers): address review feedback
rinatkhaziev Aug 21, 2026
dbf056f
refactor(edge-workers): simplify pickEnvWorkers after server-side fil…
rinatkhaziev Aug 21, 2026
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
190 changes: 190 additions & 0 deletions __tests__/bin/vip-edge-workers-deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { edgeWorkersDeployCommand } from '../../src/bin/vip-edge-workers-deploy';
import * as api from '../../src/lib/api/edge-workers';
import * as exit from '../../src/lib/cli/exit';
import * as lib from '../../src/lib/edge-workers';
import * as project from '../../src/lib/edge-workers/project';

jest.spyOn( console, 'log' ).mockImplementation( () => {} );
jest.spyOn( exit, 'withError' ).mockImplementation( () => {
throw 'EXIT_WITH_ERROR';
} );

jest.mock( '../../src/lib/cli/command', () => {
const commandMock = {
argv: () => commandMock,
examples: () => commandMock,
option: () => commandMock,
};
return jest.fn( () => commandMock );
} );

jest.mock( '../../src/lib/api/edge-workers', () => ( {
appQuery: '',
findEdgeWorkerByName: jest.fn(),
createEdgeWorker: jest.fn(),
updateEdgeWorker: jest.fn(),
validateEdgeWorker: jest.fn(),
} ) );

jest.mock( '../../src/lib/edge-workers', () => ( {
buildWorker: jest.fn(),
readPrebuiltWorker: jest.fn(),
readWorkerSource: jest.fn(),
} ) );

jest.mock( '../../src/lib/edge-workers/project', () => ( {
resolveProjectDir: jest.fn(),
findWorker: jest.fn(),
discoverWorkers: jest.fn(),
} ) );

jest.mock( '../../src/lib/tracker', () => ( {
trackEventWithEnv: jest.fn(),
} ) );

const opts = {
app: { id: 1 },
env: { id: 3 },
skipBuild: true,
};

const worker = {
dir: '/proj/workers/my-worker',
manifest: { name: 'my-worker', entry: 'assembly/index.ts', on_failure: 'continue' },
};

describe( 'edgeWorkersDeployCommand()', () => {
beforeEach( () => {
jest.clearAllMocks();
project.resolveProjectDir.mockReturnValue( '/proj' );
project.findWorker.mockReturnValue( worker );
lib.readPrebuiltWorker.mockReturnValue( {
wasmPath: '/proj/build/my-worker.wasm',
base64: 'V0FTTQ==',
sizeBytes: 5,
} );
lib.readWorkerSource.mockReturnValue( 'source code' );
api.validateEdgeWorker.mockResolvedValue( {
valid: true,
phases: [ 'client_response' ],
errors: [],
} );
} );

it( 'creates a worker when none exists with that name', async () => {
api.findEdgeWorkerByName.mockResolvedValue( null );
api.createEdgeWorker.mockResolvedValue( { id: 7, phases: [ 'response' ] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], opts );

expect( api.createEdgeWorker ).toHaveBeenCalledWith( 3, {
name: 'my-worker',
wasmBinary: 'V0FTTQ==',
onFailure: 'continue',
source: 'source code',
} );
expect( api.updateEdgeWorker ).not.toHaveBeenCalled();
} );

it( 'updates the worker when one already exists with that name', async () => {
api.findEdgeWorkerByName.mockResolvedValue( { id: 42 } );
api.updateEdgeWorker.mockResolvedValue( { id: 42, phases: [ 'response' ] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], opts );

expect( api.updateEdgeWorker ).toHaveBeenCalledWith( 3, 42, {
name: 'my-worker',
wasmBinary: 'V0FTTQ==',
onFailure: 'continue',
source: 'source code',
location: null,
} );
expect( api.createEdgeWorker ).not.toHaveBeenCalled();
} );

it( 'sends the manifest location on update, clearing it when absent', async () => {
const location = { operator: 'starts_with', value: '/api/' };
project.findWorker.mockReturnValue( {
...worker,
manifest: { ...worker.manifest, location },
} );
api.findEdgeWorkerByName.mockResolvedValue( { id: 42 } );
api.updateEdgeWorker.mockResolvedValue( { id: 42, phases: [ 'response' ] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], opts );

expect( api.updateEdgeWorker ).toHaveBeenCalledWith(
3,
42,
expect.objectContaining( { location } )
);
} );

it( 'omits location on create when the manifest has none', async () => {
api.findEdgeWorkerByName.mockResolvedValue( null );
api.createEdgeWorker.mockResolvedValue( { id: 7, phases: [] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], opts );

expect( api.createEdgeWorker ).toHaveBeenCalledWith(
3,
expect.not.objectContaining( { location: expect.anything() } )
);
} );

it( 'omits source when --skip-source is set', async () => {
api.findEdgeWorkerByName.mockResolvedValue( null );
api.createEdgeWorker.mockResolvedValue( { id: 7, phases: [] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], { ...opts, skipSource: true } );

expect( lib.readWorkerSource ).not.toHaveBeenCalled();
expect( api.createEdgeWorker ).toHaveBeenCalledWith(
3,
expect.not.objectContaining( { source: expect.anything() } )
);
} );

it( 'validates against the env before uploading', async () => {
api.findEdgeWorkerByName.mockResolvedValue( null );
api.createEdgeWorker.mockResolvedValue( { id: 7, phases: [] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], opts );

expect( api.validateEdgeWorker ).toHaveBeenCalledWith( 3, 'V0FTTQ==' );
} );

it( 'aborts the upload when validation fails', async () => {
api.validateEdgeWorker.mockResolvedValue( {
valid: false,
phases: [],
errors: [ 'missing alloc export' ],
} );

await expect( edgeWorkersDeployCommand( [ 'my-worker' ], opts ) ).rejects.toBe(
'EXIT_WITH_ERROR'
);
expect( api.createEdgeWorker ).not.toHaveBeenCalled();
expect( api.updateEdgeWorker ).not.toHaveBeenCalled();
expect( exit.withError ).toHaveBeenCalledWith(
expect.stringContaining( 'missing alloc export' )
);
} );

it( 'skips validation when --skip-validate is set', async () => {
api.findEdgeWorkerByName.mockResolvedValue( null );
api.createEdgeWorker.mockResolvedValue( { id: 7, phases: [] } );

await edgeWorkersDeployCommand( [ 'my-worker' ], { ...opts, skipValidate: true } );

expect( api.validateEdgeWorker ).not.toHaveBeenCalled();
expect( api.createEdgeWorker ).toHaveBeenCalled();
} );

it( 'errors when no worker name and no --all is given', async () => {
await expect( edgeWorkersDeployCommand( [], opts ) ).rejects.toBe( 'EXIT_WITH_ERROR' );
expect( exit.withError ).toHaveBeenCalledWith(
expect.stringContaining( 'supply a worker name' )
);
} );
} );
78 changes: 78 additions & 0 deletions __tests__/bin/vip-edge-workers-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { edgeWorkersListCommand } from '../../src/bin/vip-edge-workers-list';
import * as api from '../../src/lib/api/edge-workers';
import * as exit from '../../src/lib/cli/exit';

jest.spyOn( console, 'log' ).mockImplementation( () => {} );
jest.spyOn( exit, 'withError' ).mockImplementation( () => {
throw 'EXIT_WITH_ERROR';
} );

jest.mock( '../../src/lib/cli/command', () => {
const commandMock = {
argv: () => commandMock,
examples: () => commandMock,
option: () => commandMock,
};
return jest.fn( () => commandMock );
} );

jest.mock( '../../src/lib/api/edge-workers', () => ( {
appQuery: '',
listEdgeWorkers: jest.fn(),
} ) );

jest.mock( '../../src/lib/tracker', () => ( {
trackEventWithEnv: jest.fn(),
} ) );

const opts = { app: { id: 1 }, env: { id: 3 }, format: 'table' };

describe( 'edgeWorkersListCommand()', () => {
beforeEach( jest.clearAllMocks );

it( 'maps workers into flat, formattable rows', async () => {
api.listEdgeWorkers.mockResolvedValue( [
{
id: 5,
name: 'headers',
active: true,
phases: [ 'client_response' ],
location: { operator: 'starts_with', value: '/api/' },
onFailure: 'continue',
updatedAt: '2026-06-04',
},
] );

const rows = await edgeWorkersListCommand( [], opts );

expect( rows ).toEqual( [
{
id: 5,
name: 'headers',
active: 'yes',
phases: 'client_response',
location: 'starts_with "/api/"',
on_failure: 'continue',
modified: '2026-06-04',
},
] );
} );

it( 'shows a friendly message and returns an empty array when there are none', async () => {
api.listEdgeWorkers.mockResolvedValue( [] );

const rows = await edgeWorkersListCommand( [], opts );

expect( rows ).toEqual( [] );
expect( console.log ).toHaveBeenCalledWith(
'No edge workers are deployed to this environment.'
);
} );

it( 'reports a friendly error when the API call fails', async () => {
api.listEdgeWorkers.mockRejectedValue( new Error( 'boom' ) );

await expect( edgeWorkersListCommand( [], opts ) ).rejects.toBe( 'EXIT_WITH_ERROR' );
expect( exit.withError ).toHaveBeenCalledWith( 'Failed to list edge workers: boom' );
} );
} );
114 changes: 114 additions & 0 deletions __tests__/bin/vip-edge-workers-validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { edgeWorkersValidateCommand } from '../../src/bin/vip-edge-workers-validate';
import * as api from '../../src/lib/api/edge-workers';
import * as exit from '../../src/lib/cli/exit';
import * as lib from '../../src/lib/edge-workers';
import * as project from '../../src/lib/edge-workers/project';

jest.spyOn( console, 'log' ).mockImplementation( () => {} );
jest.spyOn( exit, 'withError' ).mockImplementation( () => {
throw 'EXIT_WITH_ERROR';
} );

jest.mock( '../../src/lib/cli/command', () => {
const commandMock = {
argv: () => commandMock,
examples: () => commandMock,
option: () => commandMock,
};
return jest.fn( () => commandMock );
} );

jest.mock( '../../src/lib/api/edge-workers', () => ( {
appQuery: '',
validateEdgeWorker: jest.fn(),
} ) );

jest.mock( '../../src/lib/edge-workers', () => ( {
buildWorker: jest.fn(),
readPrebuiltWorker: jest.fn(),
} ) );

jest.mock( '../../src/lib/edge-workers/project', () => ( {
resolveProjectDir: jest.fn(),
findWorker: jest.fn(),
discoverWorkers: jest.fn(),
} ) );

jest.mock( '../../src/lib/tracker', () => ( {
trackEventWithEnv: jest.fn(),
} ) );

const opts = { app: { id: 1 }, env: { id: 3 } };

const worker = {
dir: '/proj/workers/my-worker',
manifest: { name: 'my-worker', entry: 'assembly/index.ts' },
};

describe( 'edgeWorkersValidateCommand()', () => {
beforeEach( () => {
jest.clearAllMocks();
project.resolveProjectDir.mockReturnValue( '/proj' );
project.findWorker.mockReturnValue( worker );
lib.buildWorker.mockReturnValue( {
wasmPath: '/proj/build/my-worker.wasm',
base64: 'V0FTTQ==',
} );
api.validateEdgeWorker.mockResolvedValue( {
valid: true,
phases: [ 'client_response' ],
errors: [],
} );
} );

it( 'builds and validates the worker against the env', async () => {
await edgeWorkersValidateCommand( [ 'my-worker' ], opts );

expect( lib.buildWorker ).toHaveBeenCalledWith( '/proj', worker );
expect( api.validateEdgeWorker ).toHaveBeenCalledWith( 3, 'V0FTTQ==' );
expect( exit.withError ).not.toHaveBeenCalled();
} );

it( 'uses the prebuilt artifact with --skip-build', async () => {
lib.readPrebuiltWorker.mockReturnValue( {
wasmPath: '/proj/build/my-worker.wasm',
base64: 'UFJF',
} );

await edgeWorkersValidateCommand( [ 'my-worker' ], { ...opts, skipBuild: true } );

expect( lib.buildWorker ).not.toHaveBeenCalled();
expect( api.validateEdgeWorker ).toHaveBeenCalledWith( 3, 'UFJF' );
} );

it( 'exits with an error when a worker is invalid', async () => {
api.validateEdgeWorker.mockResolvedValue( {
valid: false,
phases: [],
errors: [ 'missing alloc export' ],
} );

await expect( edgeWorkersValidateCommand( [ 'my-worker' ], opts ) ).rejects.toBe(
'EXIT_WITH_ERROR'
);
expect( exit.withError ).toHaveBeenCalledWith( expect.stringContaining( 'failed validation' ) );
} );

it( 'validates every worker with --all', async () => {
project.discoverWorkers.mockReturnValue( [
worker,
{ dir: '/proj/workers/other', manifest: { name: 'other', entry: 'assembly/index.ts' } },
] );

await edgeWorkersValidateCommand( [], { ...opts, all: true } );

expect( api.validateEdgeWorker ).toHaveBeenCalledTimes( 2 );
} );

it( 'errors when no worker name and no --all is given', async () => {
await expect( edgeWorkersValidateCommand( [], opts ) ).rejects.toBe( 'EXIT_WITH_ERROR' );
expect( exit.withError ).toHaveBeenCalledWith(
expect.stringContaining( 'supply a worker name' )
);
} );
} );
Loading