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
5 changes: 5 additions & 0 deletions apps/cli/scripts/export-sdk-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ const INTENT_NAMES = {
'doc.toc.configure': 'configure_table_of_contents',
'doc.toc.update': 'update_table_of_contents',
'doc.toc.remove': 'remove_table_of_contents',
'doc.toc.markEntry': 'mark_table_of_contents_entry',
'doc.toc.unmarkEntry': 'unmark_table_of_contents_entry',
'doc.toc.listEntries': 'list_table_of_contents_entries',
'doc.toc.getEntry': 'get_table_of_contents_entry',
'doc.toc.editEntry': 'edit_table_of_contents_entry',
'doc.query.match': 'query_match',
'doc.mutations.preview': 'preview_mutations',
'doc.mutations.apply': 'apply_mutations',
Expand Down
150 changes: 150 additions & 0 deletions apps/cli/src/__tests__/conformance/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type OperationScenario = {
success: (harness: ConformanceHarness) => Promise<ScenarioInvocation>;
failure: (harness: ConformanceHarness) => Promise<ScenarioInvocation>;
expectedFailureCodes: string[];
skipRuntimeConformance?: boolean;
};

function commandTokens(operationId: CliOperationId): string[] {
Expand Down Expand Up @@ -421,6 +422,73 @@ function tocReadWithTargetScenario(op: string): (harness: ConformanceHarness) =>
};
}

type TocEntryAddress = {
kind: 'inline';
nodeType: 'tableOfContentsEntry';
nodeId: string;
};

function buildTocEntryInsertionTarget(paragraphNodeId: string): Record<string, unknown> {
return {
kind: 'inline-insert',
anchor: {
nodeType: 'paragraph',
nodeId: paragraphNodeId,
},
position: 'end',
};
}

async function createDocWithMarkedTocEntry(
harness: ConformanceHarness,
stateDir: string,
label: string,
): Promise<{ docPath: string; entryAddress: TocEntryAddress }> {
const sourceDoc = await harness.copyFixtureDoc(`${label}-source`);
const textTarget = await harness.firstTextRange(sourceDoc, stateDir);
const markedDoc = harness.createOutputPath(`${label}-marked`);

const mark = await harness.runCli(
[
...commandTokens('doc.toc.markEntry'),
sourceDoc,
'--target-json',
JSON.stringify(buildTocEntryInsertionTarget(textTarget.blockId)),
'--text',
'Conformance TC Entry',
'--level',
'2',
'--out',
markedDoc,
],
stateDir,
);
if (mark.result.code !== 0 || mark.envelope.ok !== true) {
throw new Error(`Failed to seed toc entry fixture for ${label}.`);
}

const listed = await harness.runCli([...commandTokens('doc.toc.listEntries'), markedDoc, '--limit', '1'], stateDir);
if (listed.result.code !== 0 || listed.envelope.ok !== true) {
throw new Error(`Failed to list toc entries for ${label}.`);
}

const entryAddress = (
listed.envelope.data as {
result?: {
items?: Array<{
address?: TocEntryAddress;
}>;
};
}
).result?.items?.[0]?.address;

if (!entryAddress) {
throw new Error(`No toc entry address found for ${label}.`);
}

return { docPath: markedDoc, entryAddress };
}

export const SUCCESS_SCENARIOS = {
'doc.open': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-open-success');
Expand Down Expand Up @@ -1364,6 +1432,81 @@ export const SUCCESS_SCENARIOS = {
'doc.toc.configure': tocMutationScenario('toc.configure', ['--patch-json', JSON.stringify({ hyperlinks: false })]),
'doc.toc.update': tocMutationScenario('toc.update', []),
'doc.toc.remove': tocMutationScenario('toc.remove', []),
'doc.toc.markEntry': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-toc-mark-entry-success');
const docPath = await harness.copyFixtureDoc('doc-toc-mark-entry');
const textTarget = await harness.firstTextRange(docPath, stateDir);
return {
stateDir,
args: [
...commandTokens('doc.toc.markEntry'),
docPath,
'--target-json',
JSON.stringify(buildTocEntryInsertionTarget(textTarget.blockId)),
'--text',
'Conformance mark-entry',
'--level',
'2',
'--table-identifier',
'A',
'--out',
harness.createOutputPath('doc-toc-mark-entry-output'),
],
};
},
'doc.toc.unmarkEntry': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-toc-unmark-entry-success');
const fixture = await createDocWithMarkedTocEntry(harness, stateDir, 'doc-toc-unmark-entry');
return {
stateDir,
args: [
...commandTokens('doc.toc.unmarkEntry'),
fixture.docPath,
'--target-json',
JSON.stringify(fixture.entryAddress),
'--out',
harness.createOutputPath('doc-toc-unmark-entry-output'),
],
};
},
'doc.toc.listEntries': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-toc-list-entries-success');
const docPath = await harness.copyFixtureDoc('doc-toc-list-entries');
return {
stateDir,
args: [...commandTokens('doc.toc.listEntries'), docPath, '--limit', '10'],
};
},
'doc.toc.getEntry': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-toc-get-entry-success');
const fixture = await createDocWithMarkedTocEntry(harness, stateDir, 'doc-toc-get-entry');
return {
stateDir,
args: [
...commandTokens('doc.toc.getEntry'),
fixture.docPath,
'--target-json',
JSON.stringify(fixture.entryAddress),
],
};
},
'doc.toc.editEntry': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-toc-edit-entry-success');
const fixture = await createDocWithMarkedTocEntry(harness, stateDir, 'doc-toc-edit-entry');
return {
stateDir,
args: [
...commandTokens('doc.toc.editEntry'),
fixture.docPath,
'--target-json',
JSON.stringify(fixture.entryAddress),
'--patch-json',
JSON.stringify({ text: 'Edited Conformance TC Entry', level: 3 }),
'--out',
harness.createOutputPath('doc-toc-edit-entry-output'),
],
};
},
'doc.session.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-session-list-success');
await harness.openSessionFixture(stateDir, 'doc-session-list', 'session-list-success');
Expand Down Expand Up @@ -1575,12 +1718,19 @@ export const SUCCESS_SCENARIOS = {
},
} as const satisfies Record<CliOperationId, (harness: ConformanceHarness) => Promise<ScenarioInvocation>>;

const RUNTIME_CONFORMANCE_SKIP = new Set<CliOperationId>([
'doc.toc.unmarkEntry',
'doc.toc.getEntry',
'doc.toc.editEntry',
]);

export const OPERATION_SCENARIOS = (Object.keys(SUCCESS_SCENARIOS) as CliOperationId[]).map((operationId) => {
const scenario: OperationScenario = {
operationId,
success: SUCCESS_SCENARIOS[operationId],
failure: genericInvalidArgumentFailure(operationId),
expectedFailureCodes: ['INVALID_ARGUMENT', 'MISSING_REQUIRED'],
...(RUNTIME_CONFORMANCE_SKIP.has(operationId) ? { skipRuntimeConformance: true } : {}),
};
return scenario;
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ describe('contract response conformance', () => {

for (const scenario of OPERATION_SCENARIOS) {
const commandKey = CLI_OPERATION_COMMAND_KEYS[scenario.operationId];
const runtimeTest = scenario.skipRuntimeConformance ? test.skip : test;

test(`success envelope conforms for ${scenario.operationId}`, async () => {
runtimeTest(`success envelope conforms for ${scenario.operationId}`, async () => {
const invocation = await scenario.success(harness);
const { result, envelope } = await harness.runCli(invocation.args, invocation.stateDir, invocation.stdinBytes);

Expand All @@ -45,7 +46,7 @@ describe('contract response conformance', () => {
}
});

test(`failure envelope conforms for ${scenario.operationId}`, async () => {
runtimeTest(`failure envelope conforms for ${scenario.operationId}`, async () => {
const invocation = await scenario.failure(harness);
const { result, envelope } = await harness.runCli(invocation.args, invocation.stateDir, invocation.stdinBytes);

Expand Down
20 changes: 20 additions & 0 deletions apps/cli/src/cli/operation-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ export const SUCCESS_VERB: Record<CliExposedOperationId, string> = {
'toc.configure': 'configured table of contents',
'toc.update': 'updated table of contents',
'toc.remove': 'removed table of contents',
'toc.markEntry': 'marked table of contents entry',
'toc.unmarkEntry': 'unmarked table of contents entry',
'toc.listEntries': 'listed table of contents entries',
'toc.getEntry': 'resolved table of contents entry',
'toc.editEntry': 'edited table of contents entry',
'query.match': 'matched selectors',
'mutations.preview': 'previewed mutations',
'mutations.apply': 'applied mutations',
Expand Down Expand Up @@ -224,6 +229,11 @@ export const OUTPUT_FORMAT: Record<CliExposedOperationId, OutputFormat> = {
'toc.configure': 'plain',
'toc.update': 'plain',
'toc.remove': 'plain',
'toc.markEntry': 'plain',
'toc.unmarkEntry': 'plain',
'toc.listEntries': 'plain',
'toc.getEntry': 'plain',
'toc.editEntry': 'plain',
'query.match': 'plain',
'mutations.preview': 'plain',
'mutations.apply': 'plain',
Expand Down Expand Up @@ -324,6 +334,11 @@ export const RESPONSE_ENVELOPE_KEY: Record<CliExposedOperationId, string | null>
'toc.configure': 'result',
'toc.update': 'result',
'toc.remove': 'result',
'toc.markEntry': 'result',
'toc.unmarkEntry': 'result',
'toc.listEntries': 'result',
'toc.getEntry': 'result',
'toc.editEntry': 'result',
'query.match': 'result',
'mutations.preview': 'result',
'mutations.apply': 'result',
Expand Down Expand Up @@ -452,6 +467,11 @@ export const OPERATION_FAMILY: Record<CliExposedOperationId, OperationFamily> =
'toc.configure': 'toc',
'toc.update': 'toc',
'toc.remove': 'toc',
'toc.markEntry': 'toc',
'toc.unmarkEntry': 'toc',
'toc.listEntries': 'query',
'toc.getEntry': 'query',
'toc.editEntry': 'toc',
'query.match': 'query',
'mutations.preview': 'general',
'mutations.apply': 'general',
Expand Down
14 changes: 14 additions & 0 deletions apps/cli/src/lib/error-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ function mapTocError(operationId: CliExposedOperationId, error: unknown, code: s
return new CliError('INVALID_ARGUMENT', message, { operationId, details });
}

if (code === 'INVALID_INPUT') {
return new CliError('INVALID_INPUT', message, { operationId, details });
}

if (code === 'CAPABILITY_UNAVAILABLE') {
return new CliError('CAPABILITY_UNAVAILABLE', message, { operationId, details });
}

if (code === 'COMMAND_UNAVAILABLE') {
return new CliError('COMMAND_FAILED', message, { operationId, details });
}
Expand Down Expand Up @@ -443,6 +451,12 @@ export function mapFailedReceipt(operationId: CliExposedOperationId, result: unk
if (failureCode === 'INVALID_TARGET') {
return new CliError('INVALID_ARGUMENT', failureMessage, { operationId, failure });
}
if (failureCode === 'PAGE_NUMBERS_NOT_MATERIALIZED') {
return new CliError('PAGE_NUMBERS_NOT_MATERIALIZED', failureMessage, { operationId, failure });
}
if (failureCode === 'CAPABILITY_UNAVAILABLE') {
return new CliError('CAPABILITY_UNAVAILABLE', failureMessage, { operationId, failure });
}
return new CliError('COMMAND_FAILED', failureMessage, { operationId, failure });
}

Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export type CliErrorCode =
| 'MATCH_NOT_FOUND'
| 'PRECONDITION_FAILED'
| 'CROSS_BLOCK_MATCH'
| 'SPAN_FRAGMENTED';
| 'SPAN_FRAGMENTED'
| 'PAGE_NUMBERS_NOT_MATERIALIZED'
| 'CAPABILITY_UNAVAILABLE';

/**
* Intersection type for errors thrown by document-api adapter operations.
Expand Down
7 changes: 6 additions & 1 deletion apps/docs/document-api/available-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Use the tables below to see what operations are available and where each one is
| Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) |
| Sections | 18 | 0 | 18 | [Reference](/document-api/reference/sections/index) |
| Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) |
| Table of Contents | 5 | 0 | 5 | [Reference](/document-api/reference/toc/index) |
| Table of Contents | 10 | 0 | 10 | [Reference](/document-api/reference/toc/index) |
| Tables | 39 | 0 | 39 | [Reference](/document-api/reference/tables/index) |
| Track Changes | 3 | 0 | 3 | [Reference](/document-api/reference/track-changes/index) |

Expand Down Expand Up @@ -156,6 +156,11 @@ Use the tables below to see what operations are available and where each one is
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.configure(...)</code></span> | [`toc.configure`](/document-api/reference/toc/configure) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.update(...)</code></span> | [`toc.update`](/document-api/reference/toc/update) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.remove(...)</code></span> | [`toc.remove`](/document-api/reference/toc/remove) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.markEntry(...)</code></span> | [`toc.markEntry`](/document-api/reference/toc/mark-entry) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.unmarkEntry(...)</code></span> | [`toc.unmarkEntry`](/document-api/reference/toc/unmark-entry) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.listEntries(...)</code></span> | [`toc.listEntries`](/document-api/reference/toc/list-entries) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.getEntry(...)</code></span> | [`toc.getEntry`](/document-api/reference/toc/get-entry) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.toc.editEntry(...)</code></span> | [`toc.editEntry`](/document-api/reference/toc/edit-entry) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.tables.convertFromText(...)</code></span> | [`tables.convertFromText`](/document-api/reference/tables/convert-from-text) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.tables.delete(...)</code></span> | [`tables.delete`](/document-api/reference/tables/delete) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.tables.clearContents(...)</code></span> | [`tables.clearContents`](/document-api/reference/tables/clear-contents) |
Expand Down
20 changes: 18 additions & 2 deletions apps/docs/document-api/reference/_generated-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,15 @@
"apps/docs/document-api/reference/tables/split.mdx",
"apps/docs/document-api/reference/tables/unmerge-cells.mdx",
"apps/docs/document-api/reference/toc/configure.mdx",
"apps/docs/document-api/reference/toc/edit-entry.mdx",
"apps/docs/document-api/reference/toc/get-entry.mdx",
"apps/docs/document-api/reference/toc/get.mdx",
"apps/docs/document-api/reference/toc/index.mdx",
"apps/docs/document-api/reference/toc/list-entries.mdx",
"apps/docs/document-api/reference/toc/list.mdx",
"apps/docs/document-api/reference/toc/mark-entry.mdx",
"apps/docs/document-api/reference/toc/remove.mdx",
"apps/docs/document-api/reference/toc/unmark-entry.mdx",
"apps/docs/document-api/reference/toc/update.mdx",
"apps/docs/document-api/reference/track-changes/decide.mdx",
"apps/docs/document-api/reference/track-changes/get.mdx",
Expand Down Expand Up @@ -437,11 +442,22 @@
{
"aliasMemberPaths": [],
"key": "toc",
"operationIds": ["toc.list", "toc.get", "toc.configure", "toc.update", "toc.remove"],
"operationIds": [
"toc.list",
"toc.get",
"toc.configure",
"toc.update",
"toc.remove",
"toc.markEntry",
"toc.unmarkEntry",
"toc.listEntries",
"toc.getEntry",
"toc.editEntry"
],
"pagePath": "apps/docs/document-api/reference/toc/index.mdx",
"title": "Table of Contents"
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "c5cf08d833b08c281a2bb18ec03caa6d95d20f69defe7897b30a9b903774705c"
"sourceHash": "510c744b41dd56592b2996c8f76683a2277c8a8025087fe107401a1277bf68ea"
}
Loading
Loading