Skip to content

Commit

Permalink
Adds command spp model remove. Closes #6118
Browse files Browse the repository at this point in the history
  • Loading branch information
mkm17 committed Nov 21, 2024
1 parent 6c6be23 commit 3e2c005
Show file tree
Hide file tree
Showing 6 changed files with 415 additions and 4 deletions.
57 changes: 57 additions & 0 deletions docs/docs/cmd/spp/model/model-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spp model remove

Deletes a document understanding model

## Usage

```sh
m365 spp model remove [options]
```

## Options

```md definition-list
`-u, --siteUrl <siteUrl>`
: The URL of the content center site.

`-i, --id [id]`
: The unique ID of the model. Specify either `id` or `title` but not both.

`-t, --title [title]`
: The display name of the model. Specify either `id` or `title` but not both.

`-f, --force`
: Don't prompt for confirmation.
```

<Global />

## Remarks

:::note

The model must be removed from all libraries before it can be deleted.

:::

## Examples

Delete a SharePoint Premium document understanding model using the model’s UniqueId.

```sh
m365 spp model remove --siteUrl "https://contoso.sharepoint.com/sites/ContentCenter" --id "7645e69d-21fb-4a24-a17a-9bdfa7cb63dc"
```

Delete a SharePoint Premium document understanding model using the model’s title.

```sh
m365 spp model remove --siteUrl "https://contoso.sharepoint.com/sites/ContentCenter" --title "climicrosoft365Model.classifier"
```

## Response

The command won't return a response on success.
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3993,6 +3993,11 @@ const sidebars: SidebarsConfig = {
type: 'doc',
label: 'model list',
id: 'cmd/spp/model/model-list'
},
{
type: 'doc',
label: 'model remove',
id: 'cmd/spp/model/model-remove'
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion src/m365/spp/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ const prefix: string = 'spp';

export default {
CONTENTCENTER_LIST: `${prefix} contentcenter list`,
MODEL_LIST: `${prefix} model list`
MODEL_LIST: `${prefix} model list`,
MODEL_REMOVE: `${prefix} model remove`
};
202 changes: 202 additions & 0 deletions src/m365/spp/commands/model/model-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './model-remove.js';
import { spp } from '../../../../utils/spp.js';

describe(commands.MODEL_REMOVE, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
sinon.stub(spp, 'assertSiteIsContentCenter').resolves();
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
});

afterEach(() => {
sinonUtil.restore([
request.delete,
cli.promptForConfirmation
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.MODEL_REMOVE);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('passes validation when required parameters are valid with id', async () => {
const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com/sites/sales', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when required parameters are valid with title', async () => {
const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com/sites/sales', title: 'ModelName' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when required parameters are valid with id and force', async () => {
const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com/sites/sales', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', force: true } }, commandInfo);
assert.strictEqual(actual, true);
});

it('fails validation when siteUrl is not valid', async () => {
const actual = await command.validate({ options: { siteUrl: 'invalidUrl', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation when id is not valid', async () => {
const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com/sites/sales', id: 'foo' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('correctly handles an error when the model id is not found', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbyuniqueid('9b1b1e42-794b-4c71-93ac-5ed92488b67f')`) {
throw {
error: {
"odata.error": {
code: "-1, Microsoft.Office.Server.ContentCenter.ModelNotFoundException",
message: {
lang: "en-US",
value: "File Not Found."
}
}
}
};
}
});

await assert.rejects(command.action(logger, { options: { verbose: true, siteUrl: 'https://contoso.sharepoint.com/sites/portal', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', force: true } }),
new CommandError('File Not Found.'));
});

it('correctly handles an error when the model title is not found', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbytitle('modeltitle.classifier')`) {
return {
"odata.null": true
};
}

throw 'Invalid request';
});

await assert.rejects(command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', title: 'modelTitle.classifier', force: true } }),
new CommandError('Model not found.'));
});

it('is the confirmation prompt called with id information', async () => {
const confirmationStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } });
assert(confirmationStub.args[0][0].message.startsWith(`Are you sure you want to remove model '9b1b1e42-794b-4c71-93ac-5ed92488b67f'?`));
});

it('is the confirmation prompt called with title information', async () => {
const confirmationStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', title: 'modelTitle' } });
assert(confirmationStub.args[0][0].message.startsWith(`Are you sure you want to remove model 'modelTitle'?`));
});

it('deletes model by id', async () => {
const stubDelete = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbyuniqueid('9b1b1e42-794b-4c71-93ac-5ed92488b67f')`) {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', force: true } });
assert(stubDelete.calledOnce);
});

it('does not delete model when confirmation is not accepted', async () => {
sinon.stub(cli, 'promptForConfirmation').resolves(false);

const stubDelete = sinon.stub(request, 'delete').resolves();

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } });
assert(stubDelete.notCalled);
});

it('deletes model when the the site URL has trailing slash', async () => {
const stubDelete = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbyuniqueid('9b1b1e42-794b-4c71-93ac-5ed92488b67f')`) {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal/', id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', force: true } });
assert(stubDelete.calledOnce);
});

it('deletes model by title', async () => {
const stubDelete = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbytitle('modelname.classifier')`) {
return '';
}

throw 'Invalid request';
});

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', title: 'ModelName', force: true } });
assert(stubDelete.calledOnce);
});

it('deletes model by title with .classifier suffic', async () => {
const stubDelete = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models/getbytitle('modelname.classifier')`) {
return '';
}

throw 'Invalid request';
});

await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal', title: 'ModelName.classifier', force: true } });
assert(stubDelete.calledOnce);
});
});
Loading

0 comments on commit 3e2c005

Please sign in to comment.