@jackwener/opencli 1.7.6 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/README.zh-CN.md +14 -8
- package/cli-manifest.json +325 -11
- package/clis/51job/company.js +125 -0
- package/clis/51job/detail.js +108 -0
- package/clis/51job/hot.js +55 -0
- package/clis/51job/search.js +79 -0
- package/clis/51job/utils.js +302 -0
- package/clis/51job/utils.test.js +69 -0
- package/clis/bilibili/video.js +11 -4
- package/clis/bilibili/video.test.js +51 -0
- package/clis/chatgpt/image.js +1 -1
- package/clis/deepseek/ask.js +19 -13
- package/clis/deepseek/ask.test.js +93 -1
- package/clis/deepseek/utils.js +108 -23
- package/clis/deepseek/utils.test.js +109 -1
- package/clis/gemini/image.js +1 -1
- package/clis/instagram/download.js +1 -1
- package/clis/twitter/likes.js +3 -2
- package/clis/twitter/search.js +4 -2
- package/clis/twitter/search.test.js +4 -0
- package/clis/twitter/shared.js +28 -0
- package/clis/twitter/shared.test.js +96 -0
- package/clis/twitter/thread.js +3 -1
- package/clis/twitter/timeline.js +3 -2
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/web/read.js +25 -5
- package/clis/web/read.test.js +76 -0
- package/clis/weread/ai-outline.js +170 -0
- package/clis/weread/ai-outline.test.js +83 -0
- package/clis/weread/book.js +57 -44
- package/clis/weread/commands.test.js +24 -0
- package/clis/xiaoyuzhou/podcast-episodes.js +2 -2
- package/clis/xiaoyuzhou/podcast-episodes.test.js +78 -0
- package/dist/src/browser/analyze.d.ts +103 -0
- package/dist/src/browser/analyze.js +230 -0
- package/dist/src/browser/analyze.test.d.ts +1 -0
- package/dist/src/browser/analyze.test.js +164 -0
- package/dist/src/browser/article-extract.d.ts +57 -0
- package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
- package/dist/src/browser/article-extract.e2e.test.js +105 -0
- package/dist/src/browser/article-extract.js +169 -0
- package/dist/src/browser/article-extract.test.d.ts +1 -0
- package/dist/src/browser/article-extract.test.js +94 -0
- package/dist/src/browser/cdp.js +11 -2
- package/dist/src/browser/verify-fixture.d.ts +59 -0
- package/dist/src/browser/verify-fixture.js +213 -0
- package/dist/src/browser/verify-fixture.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.test.js +161 -0
- package/dist/src/cli.d.ts +32 -0
- package/dist/src/cli.js +333 -43
- package/dist/src/cli.test.js +257 -1
- package/dist/src/daemon.d.ts +3 -2
- package/dist/src/daemon.js +16 -4
- package/dist/src/daemon.test.d.ts +1 -0
- package/dist/src/daemon.test.js +19 -0
- package/dist/src/download/article-download.d.ts +12 -0
- package/dist/src/download/article-download.js +141 -17
- package/dist/src/download/article-download.test.js +196 -0
- package/dist/src/download/index.js +73 -86
- package/dist/src/errors.js +4 -2
- package/dist/src/errors.test.js +13 -0
- package/dist/src/launcher.d.ts +1 -1
- package/dist/src/launcher.js +3 -3
- package/dist/src/output.js +1 -1
- package/dist/src/output.test.js +6 -0
- package/package.json +5 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type FixtureExpect = {
|
|
2
|
+
rowCount?: {
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
};
|
|
6
|
+
columns?: string[];
|
|
7
|
+
types?: Record<string, string>;
|
|
8
|
+
patterns?: Record<string, string>;
|
|
9
|
+
notEmpty?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Substrings/regex fragments that MUST NOT appear in the column value.
|
|
12
|
+
*
|
|
13
|
+
* Catches silent content contamination that `notEmpty` alone misses —
|
|
14
|
+
* e.g. a `description` field that accidentally carries "address: ..." /
|
|
15
|
+
* "category: ..." fragments from sibling DOM nodes, or a `title` that
|
|
16
|
+
* bled in a navigation-breadcrumb prefix. Each entry is matched as a
|
|
17
|
+
* plain substring against the stringified column value.
|
|
18
|
+
*/
|
|
19
|
+
mustNotContain?: Record<string, string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Columns whose values must be truthy. Complements `notEmpty` (which
|
|
22
|
+
* only rejects empty-string/null/undefined) by also catching silent
|
|
23
|
+
* `|| 0` / `|| false` fallbacks in numeric/boolean fields. Fires when
|
|
24
|
+
* the value coerces to `false` in JS.
|
|
25
|
+
*/
|
|
26
|
+
mustBeTruthy?: string[];
|
|
27
|
+
};
|
|
28
|
+
export type FixtureArgs = Record<string, unknown> | unknown[];
|
|
29
|
+
export type Fixture = {
|
|
30
|
+
args?: FixtureArgs;
|
|
31
|
+
expect?: FixtureExpect;
|
|
32
|
+
};
|
|
33
|
+
export type ValidationFailure = {
|
|
34
|
+
rule: 'rowCount' | 'column' | 'type' | 'pattern' | 'notEmpty' | 'mustNotContain' | 'mustBeTruthy';
|
|
35
|
+
detail: string;
|
|
36
|
+
rowIndex?: number;
|
|
37
|
+
};
|
|
38
|
+
export type Row = Record<string, unknown>;
|
|
39
|
+
export declare function fixturePath(site: string, command: string): string;
|
|
40
|
+
export declare function loadFixture(site: string, command: string): Fixture | null;
|
|
41
|
+
export declare function writeFixture(site: string, command: string, fixture: Fixture): string;
|
|
42
|
+
/**
|
|
43
|
+
* Derive a reasonable fixture from sample output. Used by `--write-fixture`
|
|
44
|
+
* to seed a first draft the author can hand-tune.
|
|
45
|
+
*
|
|
46
|
+
* Heuristics:
|
|
47
|
+
* - rowCount.min = 1 if rows non-empty, else 0
|
|
48
|
+
* - columns = keys from the first row
|
|
49
|
+
* - types = typeof of the first row's values, with "number|string" for mixed
|
|
50
|
+
* - no auto patterns / notEmpty — author should add those deliberately
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveFixture(rows: Row[], args?: FixtureArgs): Fixture;
|
|
53
|
+
export declare function validateRows(rows: Row[], fixture: Fixture): ValidationFailure[];
|
|
54
|
+
/**
|
|
55
|
+
* Convert fixture args into argv tokens appended after the command name.
|
|
56
|
+
* - Array form is passed through verbatim (stringified), supporting positional subjects.
|
|
57
|
+
* - Object form is expanded to `--key value` pairs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function expandFixtureArgs(args: FixtureArgs | undefined): string[];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify fixture: structural expectations for `opencli browser verify` output.
|
|
3
|
+
*
|
|
4
|
+
* The adapter-author skill runbook says every published adapter must write a
|
|
5
|
+
* fixture under `~/.opencli/sites/<site>/verify/<command>.json` so later verify
|
|
6
|
+
* runs can catch shape regressions (missing columns, wrong types, bleeding
|
|
7
|
+
* values) without relying on exact content match — BBS / news / market data is
|
|
8
|
+
* too volatile for value equality.
|
|
9
|
+
*
|
|
10
|
+
* Schema:
|
|
11
|
+
* {
|
|
12
|
+
* // args can be either:
|
|
13
|
+
* // - an object of named flags: { "limit": 3 } → expands to `--limit 3`
|
|
14
|
+
* // - a raw argv array: ["123", "--limit", "3"] → passed verbatim
|
|
15
|
+
* // Use the array form for adapters that take positional subjects (e.g. <tid>, <url>, <query>).
|
|
16
|
+
* "args": { "limit": 3 },
|
|
17
|
+
* "expect": {
|
|
18
|
+
* "rowCount": { "min": 1, "max": 10 }, // inclusive bounds
|
|
19
|
+
* "columns": ["a", "b"], // every row must have these keys
|
|
20
|
+
* "types": { "a": "string", "b": "number|string" },
|
|
21
|
+
* "patterns": { "url": "^https?://" },
|
|
22
|
+
* "notEmpty": ["title", "url"], // trimmed string must be non-empty
|
|
23
|
+
* "mustNotContain": { // catch content-contamination bleed
|
|
24
|
+
* "description": ["address:", "category:"]
|
|
25
|
+
* },
|
|
26
|
+
* "mustBeTruthy": ["count"] // catch silent `|| 0` fallbacks
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as os from 'node:os';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
export function fixturePath(site, command) {
|
|
34
|
+
return path.join(os.homedir(), '.opencli', 'sites', site, 'verify', `${command}.json`);
|
|
35
|
+
}
|
|
36
|
+
export function loadFixture(site, command) {
|
|
37
|
+
const p = fixturePath(site, command);
|
|
38
|
+
if (!fs.existsSync(p))
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new Error(`Failed to parse fixture ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writeFixture(site, command, fixture) {
|
|
50
|
+
const p = fixturePath(site, command);
|
|
51
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
52
|
+
fs.writeFileSync(p, `${JSON.stringify(fixture, null, 2)}\n`, 'utf-8');
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Derive a reasonable fixture from sample output. Used by `--write-fixture`
|
|
57
|
+
* to seed a first draft the author can hand-tune.
|
|
58
|
+
*
|
|
59
|
+
* Heuristics:
|
|
60
|
+
* - rowCount.min = 1 if rows non-empty, else 0
|
|
61
|
+
* - columns = keys from the first row
|
|
62
|
+
* - types = typeof of the first row's values, with "number|string" for mixed
|
|
63
|
+
* - no auto patterns / notEmpty — author should add those deliberately
|
|
64
|
+
*/
|
|
65
|
+
export function deriveFixture(rows, args) {
|
|
66
|
+
const expect = {};
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
expect.rowCount = { min: 0 };
|
|
69
|
+
return { ...(args ? { args } : {}), expect };
|
|
70
|
+
}
|
|
71
|
+
expect.rowCount = { min: 1 };
|
|
72
|
+
const first = rows[0];
|
|
73
|
+
const columns = Object.keys(first);
|
|
74
|
+
expect.columns = columns;
|
|
75
|
+
const types = {};
|
|
76
|
+
for (const col of columns) {
|
|
77
|
+
const observed = new Set();
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const v = row[col];
|
|
80
|
+
observed.add(jsType(v));
|
|
81
|
+
}
|
|
82
|
+
types[col] = [...observed].sort().join('|');
|
|
83
|
+
}
|
|
84
|
+
expect.types = types;
|
|
85
|
+
return { ...(args ? { args } : {}), expect };
|
|
86
|
+
}
|
|
87
|
+
export function validateRows(rows, fixture) {
|
|
88
|
+
const failures = [];
|
|
89
|
+
const expect = fixture.expect;
|
|
90
|
+
if (!expect)
|
|
91
|
+
return failures;
|
|
92
|
+
if (expect.rowCount) {
|
|
93
|
+
const { min, max } = expect.rowCount;
|
|
94
|
+
if (typeof min === 'number' && rows.length < min) {
|
|
95
|
+
failures.push({ rule: 'rowCount', detail: `got ${rows.length} rows, expected at least ${min}` });
|
|
96
|
+
}
|
|
97
|
+
if (typeof max === 'number' && rows.length > max) {
|
|
98
|
+
failures.push({ rule: 'rowCount', detail: `got ${rows.length} rows, expected at most ${max}` });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const columns = expect.columns ?? [];
|
|
102
|
+
const types = expect.types ?? {};
|
|
103
|
+
const patterns = expect.patterns ?? {};
|
|
104
|
+
const notEmpty = expect.notEmpty ?? [];
|
|
105
|
+
const compiledPatterns = {};
|
|
106
|
+
for (const [col, src] of Object.entries(patterns)) {
|
|
107
|
+
try {
|
|
108
|
+
compiledPatterns[col] = new RegExp(src);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
failures.push({ rule: 'pattern', detail: `pattern for "${col}" invalid: ${err instanceof Error ? err.message : String(err)}` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
rows.forEach((row, i) => {
|
|
115
|
+
for (const col of columns) {
|
|
116
|
+
if (!(col in row)) {
|
|
117
|
+
failures.push({ rule: 'column', detail: `missing column "${col}"`, rowIndex: i });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const [col, declared] of Object.entries(types)) {
|
|
121
|
+
if (!(col in row))
|
|
122
|
+
continue;
|
|
123
|
+
const actual = jsType(row[col]);
|
|
124
|
+
if (!typeMatches(actual, declared)) {
|
|
125
|
+
failures.push({
|
|
126
|
+
rule: 'type',
|
|
127
|
+
detail: `"${col}" is ${actual}, expected ${declared}`,
|
|
128
|
+
rowIndex: i,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const [col, re] of Object.entries(compiledPatterns)) {
|
|
133
|
+
if (!(col in row))
|
|
134
|
+
continue;
|
|
135
|
+
const v = row[col];
|
|
136
|
+
if (v === null || v === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
if (!re.test(String(v))) {
|
|
139
|
+
failures.push({
|
|
140
|
+
rule: 'pattern',
|
|
141
|
+
detail: `"${col}"=${JSON.stringify(String(v).slice(0, 60))} does not match /${re.source}/`,
|
|
142
|
+
rowIndex: i,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const col of notEmpty) {
|
|
147
|
+
const v = row[col];
|
|
148
|
+
if (v === null || v === undefined || String(v).trim() === '') {
|
|
149
|
+
failures.push({ rule: 'notEmpty', detail: `"${col}" is empty`, rowIndex: i });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const [col, needles] of Object.entries(expect.mustNotContain ?? {})) {
|
|
153
|
+
if (!(col in row))
|
|
154
|
+
continue;
|
|
155
|
+
const v = row[col];
|
|
156
|
+
if (v === null || v === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
const haystack = String(v);
|
|
159
|
+
for (const needle of needles) {
|
|
160
|
+
if (haystack.includes(needle)) {
|
|
161
|
+
failures.push({
|
|
162
|
+
rule: 'mustNotContain',
|
|
163
|
+
detail: `"${col}" contains forbidden substring ${JSON.stringify(needle)}`,
|
|
164
|
+
rowIndex: i,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const col of expect.mustBeTruthy ?? []) {
|
|
170
|
+
if (!(col in row))
|
|
171
|
+
continue;
|
|
172
|
+
if (!row[col]) {
|
|
173
|
+
failures.push({
|
|
174
|
+
rule: 'mustBeTruthy',
|
|
175
|
+
detail: `"${col}" is falsy (${JSON.stringify(row[col])}) — likely silent fallback`,
|
|
176
|
+
rowIndex: i,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return failures;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Convert fixture args into argv tokens appended after the command name.
|
|
185
|
+
* - Array form is passed through verbatim (stringified), supporting positional subjects.
|
|
186
|
+
* - Object form is expanded to `--key value` pairs.
|
|
187
|
+
*/
|
|
188
|
+
export function expandFixtureArgs(args) {
|
|
189
|
+
if (!args)
|
|
190
|
+
return [];
|
|
191
|
+
if (Array.isArray(args))
|
|
192
|
+
return args.map((v) => String(v));
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [k, v] of Object.entries(args)) {
|
|
195
|
+
out.push(`--${k}`, String(v));
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function jsType(v) {
|
|
200
|
+
if (v === null)
|
|
201
|
+
return 'null';
|
|
202
|
+
if (Array.isArray(v))
|
|
203
|
+
return 'array';
|
|
204
|
+
return typeof v;
|
|
205
|
+
}
|
|
206
|
+
function typeMatches(actual, declared) {
|
|
207
|
+
const allowed = declared.split('|').map((s) => s.trim()).filter(Boolean);
|
|
208
|
+
if (allowed.length === 0)
|
|
209
|
+
return true;
|
|
210
|
+
if (allowed.includes('any'))
|
|
211
|
+
return true;
|
|
212
|
+
return allowed.includes(actual);
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { deriveFixture, expandFixtureArgs, validateRows } from './verify-fixture.js';
|
|
3
|
+
describe('validateRows', () => {
|
|
4
|
+
it('passes when rows meet all expectations', () => {
|
|
5
|
+
const fixture = {
|
|
6
|
+
expect: {
|
|
7
|
+
rowCount: { min: 1, max: 3 },
|
|
8
|
+
columns: ['id', 'title', 'url'],
|
|
9
|
+
types: { id: 'number', title: 'string', url: 'string' },
|
|
10
|
+
patterns: { url: '^https://' },
|
|
11
|
+
notEmpty: ['title', 'url'],
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const rows = [
|
|
15
|
+
{ id: 1, title: 'a', url: 'https://x.com/a' },
|
|
16
|
+
{ id: 2, title: 'b', url: 'https://x.com/b' },
|
|
17
|
+
];
|
|
18
|
+
expect(validateRows(rows, fixture)).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it('reports rowCount below min', () => {
|
|
21
|
+
const failures = validateRows([], { expect: { rowCount: { min: 1 } } });
|
|
22
|
+
expect(failures).toHaveLength(1);
|
|
23
|
+
expect(failures[0]).toMatchObject({ rule: 'rowCount' });
|
|
24
|
+
expect(failures[0].detail).toContain('at least 1');
|
|
25
|
+
});
|
|
26
|
+
it('reports rowCount above max', () => {
|
|
27
|
+
const failures = validateRows([{}, {}, {}, {}], { expect: { rowCount: { max: 3 } } });
|
|
28
|
+
expect(failures).toHaveLength(1);
|
|
29
|
+
expect(failures[0].detail).toContain('at most 3');
|
|
30
|
+
});
|
|
31
|
+
it('reports missing columns per row', () => {
|
|
32
|
+
const failures = validateRows([{ a: 1 }, { a: 2, b: 3 }], { expect: { columns: ['a', 'b'] } });
|
|
33
|
+
// row 0 missing 'b', row 1 complete
|
|
34
|
+
expect(failures).toEqual([
|
|
35
|
+
{ rule: 'column', detail: 'missing column "b"', rowIndex: 0 },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
it('reports type mismatch including null', () => {
|
|
39
|
+
const failures = validateRows([{ a: 'abc' }, { a: null }, { a: 42 }], { expect: { types: { a: 'string' } } });
|
|
40
|
+
// row 0 string ok, row 1 null fail, row 2 number fail
|
|
41
|
+
expect(failures).toHaveLength(2);
|
|
42
|
+
expect(failures[0].rowIndex).toBe(1);
|
|
43
|
+
expect(failures[0].detail).toContain('null');
|
|
44
|
+
expect(failures[1].rowIndex).toBe(2);
|
|
45
|
+
expect(failures[1].detail).toContain('number');
|
|
46
|
+
});
|
|
47
|
+
it('accepts union types like "number|string"', () => {
|
|
48
|
+
const failures = validateRows([{ id: 1 }, { id: 'abc' }], { expect: { types: { id: 'number|string' } } });
|
|
49
|
+
expect(failures).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
it('accepts "any" as wildcard type', () => {
|
|
52
|
+
const failures = validateRows([{ v: 1 }, { v: 'x' }, { v: null }, { v: [1, 2] }], { expect: { types: { v: 'any' } } });
|
|
53
|
+
expect(failures).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
it('reports pattern mismatch with row index and truncated value', () => {
|
|
56
|
+
const failures = validateRows([{ url: 'https://ok.com' }, { url: 'not-a-url' }], { expect: { patterns: { url: '^https?://' } } });
|
|
57
|
+
expect(failures).toHaveLength(1);
|
|
58
|
+
expect(failures[0]).toMatchObject({ rule: 'pattern', rowIndex: 1 });
|
|
59
|
+
expect(failures[0].detail).toContain('not-a-url');
|
|
60
|
+
});
|
|
61
|
+
it('skips pattern check for null/undefined values', () => {
|
|
62
|
+
const failures = validateRows([{ url: null }, { url: undefined }], { expect: { patterns: { url: '^x' } } });
|
|
63
|
+
expect(failures).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it('reports invalid regex without crashing', () => {
|
|
66
|
+
const failures = validateRows([{ a: 'x' }], { expect: { patterns: { a: '[unclosed' } } });
|
|
67
|
+
expect(failures.some((f) => f.rule === 'pattern' && f.detail.includes('invalid'))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('treats empty/whitespace/null as failing notEmpty', () => {
|
|
70
|
+
const failures = validateRows([{ t: '' }, { t: ' ' }, { t: null }, { t: 'ok' }], { expect: { notEmpty: ['t'] } });
|
|
71
|
+
expect(failures).toHaveLength(3);
|
|
72
|
+
expect(failures.map((f) => f.rowIndex)).toEqual([0, 1, 2]);
|
|
73
|
+
});
|
|
74
|
+
it('no failures when fixture has no expect block', () => {
|
|
75
|
+
expect(validateRows([{ anything: 1 }], {})).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
it('mustNotContain flags substring bleed in columns', () => {
|
|
78
|
+
const failures = validateRows([
|
|
79
|
+
{ description: 'Lead engineer, 5 years exp. address: Shanghai. category: IT' },
|
|
80
|
+
{ description: 'Clean text.' },
|
|
81
|
+
], {
|
|
82
|
+
expect: {
|
|
83
|
+
mustNotContain: { description: ['address:', 'category:'] },
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(failures).toHaveLength(2);
|
|
87
|
+
expect(failures.every((f) => f.rule === 'mustNotContain')).toBe(true);
|
|
88
|
+
expect(failures.every((f) => f.rowIndex === 0)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('mustNotContain skips null/undefined values', () => {
|
|
91
|
+
const failures = validateRows([{ description: null }, { description: undefined }], { expect: { mustNotContain: { description: ['x'] } } });
|
|
92
|
+
expect(failures).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('mustBeTruthy catches silent 0 / false / "" fallbacks', () => {
|
|
95
|
+
const failures = validateRows([{ count: 10 }, { count: 0 }, { count: false }, { count: '' }, { count: null }], { expect: { mustBeTruthy: ['count'] } });
|
|
96
|
+
expect(failures).toHaveLength(4);
|
|
97
|
+
expect(failures.every((f) => f.rule === 'mustBeTruthy')).toBe(true);
|
|
98
|
+
expect(failures.map((f) => f.rowIndex)).toEqual([1, 2, 3, 4]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('deriveFixture', () => {
|
|
102
|
+
it('returns rowCount.min=0 when rows are empty', () => {
|
|
103
|
+
expect(deriveFixture([])).toEqual({ expect: { rowCount: { min: 0 } } });
|
|
104
|
+
});
|
|
105
|
+
it('extracts columns from first row and infers types per column', () => {
|
|
106
|
+
const fixture = deriveFixture([
|
|
107
|
+
{ id: 1, title: 'a', url: 'https://x' },
|
|
108
|
+
{ id: 2, title: 'b', url: 'https://y' },
|
|
109
|
+
]);
|
|
110
|
+
expect(fixture.expect?.columns).toEqual(['id', 'title', 'url']);
|
|
111
|
+
expect(fixture.expect?.types).toEqual({
|
|
112
|
+
id: 'number',
|
|
113
|
+
title: 'string',
|
|
114
|
+
url: 'string',
|
|
115
|
+
});
|
|
116
|
+
expect(fixture.expect?.rowCount).toEqual({ min: 1 });
|
|
117
|
+
});
|
|
118
|
+
it('unions mixed types across rows as "a|b"', () => {
|
|
119
|
+
const fixture = deriveFixture([
|
|
120
|
+
{ v: 1 },
|
|
121
|
+
{ v: 'two' },
|
|
122
|
+
{ v: null },
|
|
123
|
+
]);
|
|
124
|
+
expect(fixture.expect?.types?.v).toBe('null|number|string');
|
|
125
|
+
});
|
|
126
|
+
it('embeds args when provided', () => {
|
|
127
|
+
const fixture = deriveFixture([{ x: 1 }], { limit: 5 });
|
|
128
|
+
expect(fixture.args).toEqual({ limit: 5 });
|
|
129
|
+
});
|
|
130
|
+
it('embeds positional argv array when provided', () => {
|
|
131
|
+
const fixture = deriveFixture([{ x: 1 }], ['123', '--limit', '3']);
|
|
132
|
+
expect(fixture.args).toEqual(['123', '--limit', '3']);
|
|
133
|
+
});
|
|
134
|
+
it('does not add patterns or notEmpty automatically', () => {
|
|
135
|
+
const fixture = deriveFixture([{ a: 'x' }]);
|
|
136
|
+
expect(fixture.expect?.patterns).toBeUndefined();
|
|
137
|
+
expect(fixture.expect?.notEmpty).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('expandFixtureArgs', () => {
|
|
141
|
+
it('returns [] for undefined', () => {
|
|
142
|
+
expect(expandFixtureArgs(undefined)).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
it('expands object form as --key value pairs', () => {
|
|
145
|
+
expect(expandFixtureArgs({ limit: 3, sort: 'hot' })).toEqual(['--limit', '3', '--sort', 'hot']);
|
|
146
|
+
});
|
|
147
|
+
it('passes array form verbatim, stringifying values', () => {
|
|
148
|
+
expect(expandFixtureArgs(['123456', '--limit', 3])).toEqual(['123456', '--limit', '3']);
|
|
149
|
+
});
|
|
150
|
+
it('handles empty object and empty array', () => {
|
|
151
|
+
expect(expandFixtureArgs({})).toEqual([]);
|
|
152
|
+
expect(expandFixtureArgs([])).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
it('preserves positional + flag mix (e.g. <tid> --limit 3)', () => {
|
|
155
|
+
expect(expandFixtureArgs(['https://example.com/thread-1', '--comments', '5'])).toEqual([
|
|
156
|
+
'https://example.com/thread-1',
|
|
157
|
+
'--comments',
|
|
158
|
+
'5',
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
});
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -6,6 +6,38 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { findPackageRoot } from './package-paths.js';
|
|
9
|
+
/**
|
|
10
|
+
* Check whether the site-memory scaffolding exists under
|
|
11
|
+
* ~/.opencli/sites/<site>/. Agents have a strong tendency to forget to write
|
|
12
|
+
* endpoints.json / notes.md after a successful verify, which dooms the next
|
|
13
|
+
* agent to redo recon from scratch. Surfacing the current state as part of
|
|
14
|
+
* verify's final report converts that "silent skip" into a visible nudge;
|
|
15
|
+
* `--strict-memory` escalates it to a failure so agents driving a hardened
|
|
16
|
+
* workflow can't forget.
|
|
17
|
+
*/
|
|
18
|
+
export type SiteMemoryReport = {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
siteDir: string;
|
|
21
|
+
endpoints: {
|
|
22
|
+
present: boolean;
|
|
23
|
+
count: number;
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
notes: {
|
|
27
|
+
present: boolean;
|
|
28
|
+
path: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare function checkSiteMemory(site: string): SiteMemoryReport;
|
|
32
|
+
export declare function printSiteMemoryReport(report: SiteMemoryReport, strict: boolean | undefined): void;
|
|
33
|
+
/** Coerce adapter JSON output into a row array. Accepts `[{...}]`, single `{}`, or `{items:[...]}`-style envelopes. */
|
|
34
|
+
export declare function normalizeVerifyRows(data: unknown): Record<string, unknown>[];
|
|
35
|
+
/** Render up to 10 rows as a compact padded table for eyeball inspection during verify. */
|
|
36
|
+
export declare function renderVerifyPreview(rows: Record<string, unknown>[], opts?: {
|
|
37
|
+
maxRows?: number;
|
|
38
|
+
maxCols?: number;
|
|
39
|
+
cellMax?: number;
|
|
40
|
+
}): string;
|
|
9
41
|
export declare function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command;
|
|
10
42
|
export declare function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void;
|
|
11
43
|
export interface BrowserVerifyInvocation {
|