Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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